From: Juhani Numminen Date: Thu, 5 Jan 2017 20:14:45 +0000 (+0000) Subject: Import pentobi_12.2.orig.tar.xz X-Git-Tag: archive/raspbian/17.3-1+rpi1~2^2^2^2~1 X-Git-Url: https://dgit.raspbian.org/%22http://www.example.com/cgi/%22/%22http:/www.example.com/cgi/%22?a=commitdiff_plain;h=2c9fa8ff49ce1768b46afaaf468545ffa6d5ff01;p=pentobi.git Import pentobi_12.2.orig.tar.xz [dgit import orig pentobi_12.2.orig.tar.xz] --- 2c9fa8ff49ce1768b46afaaf468545ffa6d5ff01 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b21e9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +CMakeLists.txt.user +Pentobi.creator.user diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ff848ce --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 3.0.2) + +project(Pentobi) +set(PENTOBI_VERSION 12.2) +set(PENTOBI_RELEASE_DATE 2017-01-05) + +cmake_policy(SET CMP0043 NEW) + +include(CheckIncludeFiles) +include(GNUInstallDirs) + +option(PENTOBI_BUILD_TESTS "Build unit tests" OFF) +option(PENTOBI_BUILD_GTP "Build GTP interface" OFF) +option(PENTOBI_BUILD_GUI "Build Qt-based GUI" ON) +option(PENTOBI_BUILD_QML "Build QtQuick-based GUI" OFF) +option(PENTOBI_BUILD_KDE_THUMBNAILER "Build thumbnailer for KDE" OFF) + +if (PENTOBI_BUILD_KDE_THUMBNAILER AND NOT PENTOBI_BUILD_GUI) + message(FATAL_ERROR + "PENTOBI_BUILD_KDE_THUMBNAILER requires PENTOBI_BUILD_GUI=1") +endif() + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "No build type selected, default to Release") + set(CMAKE_BUILD_TYPE "Release") +endif() +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DLIBBOARDGAME_DEBUG") + +if(CMAKE_COMPILER_IS_GNUCXX OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + OR (CMAKE_CXX_COMPILER_ID MATCHES "Intel")) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +endif() +if(CMAKE_COMPILER_IS_GNUCXX OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffast-math") +endif() +if(MSVC) + add_definitions(-D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE + -D_SCL_SECURE_NO_WARNINGS) +endif() + +check_include_files(unistd.h HAVE_UNISTD_H) +check_include_files(sys/times.h HAVE_SYS_TIMES_H) +check_include_files(sys/sysctl.h HAVE_SYS_SYSCTL_H) + +if(NOT DEFINED LIBPENTOBI_MCTS_FLOAT_TYPE) + set(LIBPENTOBI_MCTS_FLOAT_TYPE float) +endif() + +# Don't set the Pentobi data dirs on Windows. This is currently needed for +# building a version of Pentobi for the NSIS installer on Windows (see +# directory windows) such that Pentobi will look for data dirs relative to +# the installation directory. (It breaks installing Pentobi on Windows with +# "make install" but we don't support that on Windows anyway.) +if(UNIX) + if(NOT DEFINED PENTOBI_BOOKS_DIR) + set(PENTOBI_BOOKS_DIR "${CMAKE_INSTALL_FULL_DATADIR}/pentobi/books") + endif() + if(NOT DEFINED PENTOBI_HELP_DIR) + set(PENTOBI_HELP_DIR "${CMAKE_INSTALL_FULL_DATAROOTDIR}/help") + endif() + if(NOT DEFINED PENTOBI_TRANSLATIONS) + set(PENTOBI_TRANSLATIONS + "${CMAKE_INSTALL_FULL_DATADIR}/pentobi/translations") + endif() +endif(UNIX) + +configure_file(config.h.in config.h) +add_definitions(-DHAVE_CONFIG_H) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +include_directories(${CMAKE_SOURCE_DIR}/src) + +if(PENTOBI_BUILD_TESTS) + enable_testing() +endif() + +find_package(Threads) + +if(PENTOBI_BUILD_GUI) + find_package(Qt5Concurrent 5.2 REQUIRED) + find_package(Qt5Widgets 5.2 REQUIRED) + find_package(Qt5LinguistTools 5.2 REQUIRED) + find_package(Qt5Svg 5.2 REQUIRED) +endif() +if(PENTOBI_BUILD_QML) + # Qt 5.3 is good enough for building but the QML files require Qt 5.6 to run + find_package(Qt5Concurrent 5.3 REQUIRED) + find_package(Qt5Qml 5.3 REQUIRED) + find_package(Qt5Gui 5.3 REQUIRED) + find_package(Qt5Svg 5.3 REQUIRED) +endif() + +if(UNIX) + add_custom_target(dist + COMMAND git archive --prefix=pentobi-${PENTOBI_VERSION}/ HEAD + | xz -e > ${CMAKE_BINARY_DIR}/pentobi-${PENTOBI_VERSION}.tar.xz + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) +endif() + +add_subdirectory(doc) +add_subdirectory(src) +add_subdirectory(data) +if(WIN32 AND PENTOBI_BUILD_GUI) + add_subdirectory(windows) +endif() + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..72591e1 --- /dev/null +++ b/COPYING @@ -0,0 +1,694 @@ +Copyright (C) 2011-2017 Markus Enzenberger + +Pentobi is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) +any later version. + +Pentobi is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +more details. + +A copy of the GNU General Public License version 3 is appended below. + +Trademark disclaimer: The trademark Blokus and other trademarks referred +to are property of their respective trademark holders. The trademark +holders are not affiliated with the author of the program Pentobi. + +-------------------------------------------------------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..e60f9fc --- /dev/null +++ b/INSTALL @@ -0,0 +1,60 @@ +This file explains how to compile and install Pentobi from the sources. + + +== Requirements == + +Pentobi requires the Qt libraries (>=5.2). The C++ compiler needs to support +certain C++11 features (only features that are already implemented by +GCC 4.9 and MSVC 2015). The build system uses CMake (>=3.0.2). + +Ubuntu 16.04 provides suitable versions of the required tools and libraries in +its package repository. They can be installed with the shell command: + + sudo apt-get install \ + g++ make cmake qttools5-dev qttools5-dev-tools libqt5svg5-dev + + +== Building == + +Pentobi can be compiled from the source directory with the shell commands: + + cmake -DCMAKE_BUILD_TYPE=Release . + make + + +=== Building the KDE thumbnailer plugin === + +A thumbnailer plugin for KDE can be built by using the cmake option +-DPENTOBI_BUILD_KDE_THUMBNAILER=1. In this case, the KDE development files +need to be installed (packages kio-dev and extra-cmake-modules on +Ubuntu 16.04). Note that on Ubuntu 16.04, the plugin will not be found if +the default installation prefix /usr/local is used. You need to add +QT_PLUGIN_PATH=/usr/local/lib/plugins to /etc/environment. After that, you +can enable previews for Blokus game file in the Dolphin file manager in +"Configure Dolphin/General/Previews". + + +== Installing == + +On Linux, Pentobi can be installed after compilation with the shell command: + + sudo make install + +After installation, the system-wide databases should be updated to +make Pentobi appear in the desktop menu and register it as handler for Blokus +files (*.blksgf). On Ubuntu 16.04 with install prefix /usr/local, this can be +done by running: + + sudo update-mime-database /usr/local/share/mime + sudo update-desktop-database /usr/local/share/applications + + +== Building the Android version == + +For building the Android app, there is a QtCreator project file in +src/pentobi_qml/Pentobi.pro. It requires Qt 5.6. Before compilation, the +binary translation files need to be generated by using File/Release in +Qt Linguist for all TS files in src/pentobi_qml/qml/translations. + +For testing purposes, the GUI that is used for Android can also be built as a +desktop application by running CMake with -DPENTOBI_BUILD_QML=1. diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..4667f33 --- /dev/null +++ b/NEWS @@ -0,0 +1,582 @@ +Version 12.2 (05 Jan 2017) +========================== + +General: + +* Added patterns for Nexos and Callisto SGF files to MIME type + specification for detecting them independent of the file ending. + +Desktop version: + +* Game info properties were not removed from file if the corresponding + text in the game info dialog was deleted. +* New Game/Save As was not enabled if no move had been played but game + was modified by editing the comment in the root node or the game info. +* Fixed a race condition that could cause a crash when updating the + analysis window while a game analysis was running. +* Game analysis progress dialog was not closed if analysis was canceled. + +Android version: + +* Toolbuttons were too small on very high DPI devices. +* Open/Save did not show error message on failure. + + +Version 12.1 (30 Nov 2016) +========================== + +General: + +* Loading a file with a setup position in Nexos did not always work correctly + or could cause a crash. +* SGF files for two-player Callisto did not use B/W properties as documented + but 1/2 as in multi-player variants. Files written by Pentobi 12.0 can still + be read and will be converted if saved again. + +Desktop version: + +* Compilation on Windows is no longer tested or supported. +* Keep Only Position and Keep Only Subtree did not work correctly in Nexos and + in multi-player Callisto. +* Delete All Variations did not mark the file as modified. +* Missing semicolon in desktop entry file (bug #12). +* Fixed ambiguous shortcut overload. +* Saving a file will now remember the directory and use it as a default for + file dialogs. + + +Version 12.0 (10 Apr 2016) +========================== + +General: + +* New game variant Callisto. +* Thinking time of level 7 (the highest level supported on Android) was + increased in most game variants to better match the CPU speed of + typical mobile hardware. +* Starting points are no longer shown after color played its first piece. + +Desktop version: + +* The compilation now requires at least Qt 5.2. +* High-DPI scaling is now automatically used if compiled with Qt 5.6. +* Setting Move Marking to Last now only marks the last move even if the + computer played several moves in a row. + +Bug fixes Desktop version: + +* Icon for undo did not have a high-DPI version. +* Option --verbose was broken on Windows. + +Android version: + +* The compilation now requires Qt 5.6. +* Support for game variant Nexos. +* New menu items Edit/Delete All Variations, Edit/Next Color, + View/Animate Pieces, Help/About. +* Actions with buttons in action bar are no longer shown in menu. +* Forward/backward buttons now support autorepeat. + +Bug fixes Android version: + +* Fixed crash that could occur when switching game variants while a + piece was selected. +* Level set for game variant Classic3 was ignored, instead the level set + for Classic was used. +* Move generation was not properly aborted if some Edit menu items were + selected while the computer was thinking. + + +Version 11.0 (29 Dec 2015) +========================== + +General: + +* Slightly increased playing strength, mainly in Trigon. +* The compilation requires now at least Qt 5.1 and GCC 4.9 or MSVC 2015. +* The score display now shows stars at scores that contain bonuses. + +Desktop version: + +* New game variant Nexos (2 or 4 players). +* If a piece is removed from the board in setup mode, it will now + become the selected piece. +* The command line option --memory was replaced by --maxlevel, which + reduces the needed memory and removes higher levels from the menu. +* The memory requirements are now 1 GB minimum, 4 GB recommended for + playing level 9. +* Added an application metadata file on Linux according to the AppStream + specification from freedesktop.org. Added a 64x64 app icon but no + longer an xpm icon (Debian AppStream Guidelines). + +Bug fixes desktop version: + +* Message dialog about discarding unsaved current game was not shown if + a file was loaded by clicking on a game in the rating dialog. +* Last move marking did not work anymore after after interrupting a + computer move generation and then using Undo Move. +* Autosaving unfinished games did not work if game was finished + first but then made unfinished again with Undo Move. +* Selecting pieces in setup mode did no longer work if no legal moves + were left, even if setup mode is also intended to be used for + setting up illegal positions (e.g. for Blokus art). + +Android version: + +* Initial support for loading/saving, variations and game tree navigation. +* The piece area now has enough room for all pieces of one color. It also + removes rows that become empty and orders the colors such that the color + to play is always on top. +* Action buttons and menu items are now only shown if the action is + enabled in the current position. + + +Version 10.1 (15 Oct 2015) +========================== + +Desktop version: + +* New toolbar button for Undo Move. +* Annotations are now also appended to the move number in the status line. +* Don't show move number in status line if no moves have been played. +* Show an error message instead of the crash dialog if the startup + fails due to low memory. +* The Windows installer is now built with Qt 5 and dynamic libraries. + +Android version: + +* New action bar button for Undo Move. +* Reduced memory requirements. A meaningful error message is now shown + if the startup fails due to low memory. +* Workaround for a bug that made the back button no longer exit the app + after the computer color dialog was shown (QTBUG-48456). +* Faster startup. +* Changed snapping behavior of the piece area to make it easier to flick + vertically between colors with multiple movements on small screens. + + +Version 10.0 (01 Jul 2015) +========================== + +* Increased playing strength and more opening variety in Trigon. +* The Backward10/Forward10 toolbar buttons were replaced by autorepeat + functionality of the Backward/Forward buttons. +* The last move is now by default marked with a dot instead of a number. +* The compilation now requires at least GCC 4.8 and CMake 3.0.2. +* On Linux, the manual is now installed in $PREFIX/share/help according + to the freedesktop.org help specification. +* The KDE thumbnailer plugin can now be compiled with KDE Frameworks 5. +* Better support for high resolution displays if compiled with Qt 5.1 + or newer and environment variable QT_DEVICE_PIXEL_RATIO is used. +* The Pentobi help browser now uses a larger font on Windows +* Regional language subvariants en_GB, en_CA are no longer supported. + +Bug fixes: + +* Fixed a build failure when generating the PNG icons from the SVG sources + if the path contained non-ASCII characters. +* Fixed failure to open a file given as a command line argument to pentobi + (including the case when Pentobi is used as a handler for blksgf files + in file browsers) if the path contained non-ASCII characters. +* Changed the file dialog filter for "All files" from *.* to * such that + really all files are shown even if they have no file ending. + Added an "All files" filter to the Export/ASCII Art file dialog. +* Remembering the playing level separately for each game variant did not + work if the game variant was implicitly changed by opening a file. +* "View/Move Numbers/Last" did not behave correctly after all colors were + enabled in the Computer Colors dialog while a move generation was running. +* Fixed build failure with MSVC if MinGW was not also installed (because + windres.exe was used) + + +Version 9.0 (10 Dec 2014) +========================= + +* Newly supported game variant Classic for 3 players, in which the + players take turns playing the fourth color. +* Increased playing strength, mainly in game variant Trigon. +* There are now 9 levels and the playing strength increases more evenly + with the level. Ratings in rated games are still comparable to previous + versions of Pentobi apart from Trigon at lower levels because Trigon + starts now with a higher playing strength at level 1. +* The computer is now better at playing moves that maximize the score + as long as they do not lead into riskier positions. +* The computer now remembers the playing level separately for each game + variant and restores it when the game variant is changed. +* Player ratings now change faster if less than 30 rated games have been + played, and slower afterwards. +* The mouse wheel can no longer be used for game navigation because it + was too easy to trigger accidentally while playing a game. This also + fixes the bug that the game navigation with the mouse wheel was not + disabled in rated games and the game could not be continued after that + because the play button is disabled in rated games. +* It is no longer possible to select and play a piece while the computer + is thinking, the thinking must be aborted first with Computer/Stop. +* Bugfix: program crashed if computer colors dialog was opened and closed + with OK while computer was thinking. +* Experimental support for Android. The Android version supports only a + subset of the features of the desktop version and only playing levels + 1 to 7. There are still known issues with the user interface due to + bugs in Qt for Android. The Android version is currently only available + as an APK file for devices with an ARMv7 CPU from the download section + of http://pentobi.sourceforge.net + + +Version 8.2 (05 Sep 2014) +========================= + +* Fixed remaining link errors on some platforms (Debian bug #759852) + + +Version 8.1 (31 Aug 2014) +========================= + +* Fixed link error on some platforms if Pentobi is compiled with + PENTOBI_BUILD_TESTS (Debian bug #759852) +* Slightly improved some icons and use icons from theme for more menu items + + +Version 8.0 (02 Mar 2014) +========================= + +* Increased playing strength, especially in game variant Trigon. +* Improved performance on multi-core CPUs: Previously, the move + generation was faster on multi-core CPUs but there was a small drop + in playing strength compared to the same playing level on a + single-core CPU. This effect has been reduced. +* New toolbar button for starting a rated game. +* The interface is now more locked down during rated games, for example + it is no longer possible to change the computer colors or take back a + move during a rated game. +* The menu item "Computer Colors" was moved from the Game to the + Computer menu. +* The source code no longer compiles with MSVC 2012 but requires + MSVC 2013 because a larger subset of C++11 features is used. +* The source code distribution now uses xz instead of gzip for + compression. +* The PNG versions of the icons are no longer included in the source + code but generated at build time from the SVG icons by a small + Qt-based helper program. This adds a build time dependency on QtSvg. +* A XPM icon is now installed to share/pixmaps. +* The configure option USE_BOOST_THREAD is no longer supported. + For building with MinGW, a version of MinGW with support for + std::thread is now required (e.g. from mingwbuilds.sf.net). + + +Version 7.2 (30 Jan 2014) +========================= + +* Hyphens used as minus signs in manpage (bug #9) +* Added keywords section to desktop entry to silence lintian + warning (bug #10) +* Fixed a compilation error with GCC 4.8.2 on PowerPC (and other + big-endian systems) +* Fixed wrong arguments to update-mime-database/update-desktop-database + when running "make post-install" +* Improved a blurry menu item icon +* Fixed a compilation warning about a missing translation +* Reduced the sizes of the generated and installed translation files. +* Fixed a compilation error on 64-bit Linux with X32 ABI +* Fixed a compilation error with Cygwin + + +Version 7.1 (13 Aug 2013) +========================= + +* Fixed the version string. The released file pentobi-7.0.tar.gz was + erroneously built from git version c5247c56 just before the version + tagged with v7.0 and contained the version string 6.UNKNOWN +* The color played by the human in rated games is now randomly assigned +* The mouse wheel is now disabled while the computer is thinking + + +Version 7.0 (25 Jun 2013) +========================= + +* Support for compilation with version 5 of the Qt libraries (see INSTALL + for details) +* Slightly increased playing strength at higher levels (mainly in game + variant Duo) +* The default settings in game variants with more than two players are now + that the human plays the first color and the computer all other colors +* Fixed a crash that could occur if the window was put in fullscreen mode + by a method of the window manager (e.g. title bar menu on KDE) and then + returned to normal mode by a different method (e.g. pressing Escape) + + +Version 6.0 (4 Mar 2013) +======================== + +* Increased playing strength at higher levels. The search algorithm used + for move generation is now parallelized and can take advantage of + multi-core CPUs (up to 4 cores). There is a new playing level 8, which + has a 2 GHz dual-core CPU or faster as the recommended system requirement. +* New menu item Toolbar Text to configure the toolbar button appearance + independent of the system settings +* More SGF game info properties (event, round, time) were added to the + game info dialog +* The source code now requires at least GCC 4.7 (because a larger subset + of C++11 features is used) +* The CMake module GNUInstallDirs is now used for setting the installation + directories on Unix. Note that the defaults for bindir and datadir are + now CMAKE_INSTALL_PREFIX/bin and CMAKE_INSTALL_PREFIX/share instead of + CMAKE_INSTALL_PREFIX/games and CMAKE_INSTALL_PREFIX/share/games. + They can be changed by setting CMAKE_INSTALL_BINDIR and + CMAKE_INSTALL_DATADIR (bug #7) +* The source code no longer depends on the Boost libraries. However, it + is still possible to use Boost.Thread instead of std::thread by + configuring with USE_BOOST_THREAD=ON (e.g. needed on MinGW GCC 4.7, + which has no functional implementation of std::thread) +* Thumbnailer registration for blksgf files is no longer supported for + Gnome 2 + + +Version 5.0 (10 Dec 2012) +========================= + +* Small increase in overall playing strength at higher levels in all game + variants (especially Trigon) +* The computer now knows about the possibility of rotational-symmetric tied + games in game variant Trigon Two-Player (like it already knew in the + variants Duo and Junior) and will prevent the second player from enforcing + such a tie +* If the move generation takes longer than 10 seconds, the maximum remaining + time is now shown in the status bar +* Removed less frequently used buttons (Open, Save) from the tool bar +* Re-organized menu bar +* The menu bar and tool bar are no longer shown in fullscreen mode +* Avoided some window flickering at startup + + +Version 4.3 (2 Nov 2012) +======================== + +* Setting the computer color for Red with the computer colors dialog did + not work for game variant Trigon Three-Player +* Disable Undo menu item when it is not applicable +* Fixed an assertion at end of move generation in Trigon Three-Player if + Pentobi was compiled in debug mode + + +Version 4.2 (7 Oct 2012) +======================== + +* Fixed crash when opening game info dialog in game variants Classic + Two-Player or Trigon Two-Player + + +Version 4.1 (5 Oct 2012) +======================== + +* Result of rated game was counted wrongly in four-color/two-player game + variants if the first player had a higher score than the second player + but the first color a lower score than the second color. +* Fixed potential crash if Undo, Truncate or Truncate Children is selected + while the computer is thinking. +* Automatic continuing of computer play did not work in some cases if the + computer was thinking while the Computer Color dialog was used. + + +Version 4.0 (4 Oct 2012) +======================== + +* New menu item "Beginning of Branch" +* The rating dialog now also shows the best previous rating and has + a button to reset the rating +* A thumbnail plugin for KDE can be built by using the CMake option + -DPENTOBI_BUILD_KDE_THUMBNAILER=ON +* Replaced the icons with less colorful ones. All icons are now licensed + under the GPLv3+ and include SVG sources. No icons from the Tango icon + set are used anymore. + + +Version 3.1 (2 Aug 2012) +======================== + +* Fixed a bug in version 3.0 in the replacement of obsolete move properties + in old files that corrupted files in game variants with 3 or 4 colors. + + +Version 3.0 (1 Aug 2012) +======================== + +* New functionality to compute a player rating for the user by playing + rated games against the computer +* Different options for speed of game analysis +* New menu item "Play Single Move" to make the computer play a move + without changing the colors played by the computer +* The mouse wheel can now be used to navigate in the current variation + if no piece is selected +* Files written by older versions of Pentobi that use a deprecated format + for move properties are now automatically converted to the current format + on write + + +Version 2.1 (1 Jul 2012) +======================== + +* Bugfix: File was erroneously marked as modified if a multiline comment + was shown and the platform that was used to create the file had + Windows-style end of line convention and the platform on which the file + was shown had Unix-style. +* Fixed the corruption of non-ASCII characters in game files on some + platforms. +* Fixed a case where the program froze instead of showing an error on + certain syntax errors in the SGF file. +* Fixed duplicate menu shortcut in German translation +* Fixed too high floating point tolerance in unit tests. + + +Version 2.0 (22 May 2012) +========================= + +* No more popup messages if a color has no more moves; + instead, score points of this color are underlined + (feature request #3431031) +* Newly supported game variant Junior +* Improved playing strength. Number of levels increased to 7. + Level 7 is about the same speed as the old level 6 but stronger. +* New game analysis function that shows a graph with the estimated + value of each position in a game (menu item "Computer/Analyze Game") +* Support for setup properties in blksgf files (note that files + with setup properties cannot be read by older versions of + Pentobi). A new setup mode can be used to create files that start + with a setup position including positions that cannot occur in + real games (e.g. for puzzles or Blokus art) +* New menu items for editing the game tree: "Delete All Variations", + "Keep Only Position", "Keep Only Subtree", "Move Variation Up/Down", + "Truncate Children" +* Variations are now displayed by appending a letter to the move number + instead of underlining +* Added a toolbar button for fast selection of the computer colors + without having to use the window menu. +* User manual is no longer compiled into the resources of the + executable but installed in the installation data directory +* Open a console for stderr output on Windows if Pentobi is + invoked with option --verbose +* New option --memory to make Pentobi run on systems with low + memory at the cost of reduced playing strength. +* Use standard icons from theme + + +Version 1.2 (17 Apr 2012) +========================= + +* Bugfix: program sometimes hung or crashed when generating a + move in early game Trigon positions especially when there + were no legal moves with any of the large pieces +* Bugfix: file modified marker was not set on certain changes + (Make Main Variation, comment changed) +* Bugfix: game info dialog showed wrong player labels in Trigon + and Trigon Three-Player * Minor other bugfixes in the code +* Reverted the change that used the SVG icon for setting the + window icon because it created an unwanted dependency on the + Qt SVG plugin. +* Made Save menu item and tool button active if game is modified + even if no file name is associated with the current game +* Made the code compile without warnings with GCC -Wunused +* Made "make post-install" continue even if some commands fail. + + +Version 1.1 (10 Mar 2012) +========================= + +* File is now immediately visible in Recent Files menu after + saving under a new name. +* Fixed several cases where the program crashed instead of showing + an error message if the opened file was invalid. The error + message now also has a Show Details button to show the reason + why the file could not be loaded. +* Fixed a bug that distorted the position values reported with + --verbose if a subtree from a previous search was reused +* Fixed exception in tools/twogtp/analyze.py if option -r was used +* Minor fixes in computer player engine +* Added explaining label to computer color dialog because window + title is not visible in all L&F's +* Accept pass moves (empty value) in files. Although the current + Blokus SGF documentation does not specify if they should be + allowed, they might be used in the future and are used in files + written by early (unreleased) versions of Pentobi +* Extended the file format documentation by a hint how to put + blksgf files on web servers +* Smaller icons for piece manipulation buttons +* Fixed computation of the font bounding box in the score display +* Set option -std=c++0x in CMakeLists.txt if compiler is CLang +* Removed duplicate pentobi.png in directories data and src/pentobi; + The file pentobi.svg was moved from data to src/pentobi and is + now used for setting the window icon of Pentobi + + +Version 1.0 (1 Jan 2012) +======================== + +* Support for game variant Trigon Three-Player +* Change directory for autosave file to use AppData + (on Windows) or XDG_DATA_HOME (on other systems) +* Changed Back to Main Variation to go to the last move + in the main variation that had a variation, not to the + last position in the main variation +* Changed variation string in status bar to contain + information about the move numbers at the branching points +* Fixed small rendering errors +* New menu item Find Next Comment +* Added chapters about the main window and menu items to + the user manual +* Fix bug: computer color dialog did not set colors correctly + in game variant Trigon +* Show error message instead of crashing if the SGF file + contains invalid move properties +* Lowered the required version of the Boost libraries in + CMakeLists.txt from 1.45 to 1.40 such that Pentobi can + be compiled on Debian 6.0. + Note: some versions of Boost cause compilation errors if + used with certain versions of GCC and option -std=c++0x + (e.g. the combinations GCC 4.4/Boost 1.40 in Ubuntu 10.04 + and GCC 4.4/Boost 1.42 in Debian 6.0 work but the combination + GCC 4.5/Boost 1.42 in Ubuntu 11.04 causes errors). +* Changed installation directories according to Filesystem + Hierarchy Standard (/usr/bin to /usr/games, /usr/share to + /usr/share/games) +* New CMake option PENTOBI_REGISTER_GNOME2_THUMBNAILER for + disabling the installation of files for registering the Pentobi + thumbnailer on Gnome 2 +* Install man pages for pentobi and pentobi-thumbnailer on Unix + systems + + +Version 0.3 (2 Dec 2011) +======================== + +* Support for the game variants Trigon and Trigon Two-Player +* Fixed saving/opening files if file name contained non-ASCII characters and + the system used an encoding other than Latin1 +* The score numbers now show the total player and color scores instead of + on-board and bonus points separately (feature request #3431039) +* New menu item "Edit/Select Next Color" that allows to enter moves independent + of the color to play on the board (feature request #3441299) +* Slightly changed file format to use single-valued move properties as used in + other games supported by SGF. Files written by Pentobi 0.2 can still be read. + + +Version 0.2 (17 Oct 2011) +========================= + +* German translation +* Display sum score for both player colors in game variant Classic Two-Player +* Slightly changed file format to conform to the proposed version 5 of SGF that + requires digits for move properties in multi-player games. Files written by + Pentobi 0.1 can still be read. +* Support for move annotation symbols +* Store and edit additional game information (player names, date) +* New menu items Ten Moves Backward/Forward, Go to Move, Undo Move +* Underline move numbers if there are alternative variations +* Show move number, total number of moves and current variation in status bar +* Faster play in higher levels, especially of opening moves +* Make thumbnailer for Blokus files work under Gnome 3 +* Fix broken compilation with GCC 4.6.1 (bug #3420555) + + +Version 0.1 (15 Jul 2011) +========================= + +Initial release. diff --git a/README b/README new file mode 100644 index 0000000..acb622a --- /dev/null +++ b/README @@ -0,0 +1,9 @@ +Pentobi is a computer opponent for the board game Blokus. + +Copyright (C) 2011-2016 Markus Enzenberger + +See the file COPYING for license information. +See the file INSTALL for instructions about how to build and install the +program from the sources. +See the file NEWS for release notes. +The homepage of Pentobi is at http://pentobi.sourceforge.net/ diff --git a/config.h.in b/config.h.in new file mode 100644 index 0000000..774b93e --- /dev/null +++ b/config.h.in @@ -0,0 +1,30 @@ +/* Define to 1 if you have the header file. */ +#cmakedefine01 HAVE_UNISTD_H + +/* Define to 1 if you have the header file. */ +#cmakedefine01 HAVE_SYS_TIMES_H + +/* Define to 1 if you have the header file. */ +#cmakedefine01 HAVE_SYS_SYSCTL_H + +/* Version number of package */ +#define VERSION "@PENTOBI_VERSION@" + +/* Define if the MCTS search does not need to support multi-threading. + This makes the search slightly faster on single-threaded systems. */ +#cmakedefine LIBBOARDGAME_MCTS_SINGLE_THREAD + +/* Floating type for Monte-Carlo tree search values (float|double) */ +#define LIBPENTOBI_MCTS_FLOAT_TYPE @LIBPENTOBI_MCTS_FLOAT_TYPE@ + +/* Build for systems with low memory and slow CPU. */ +#cmakedefine01 PENTOBI_LOW_RESOURCES + +/* Directory containing opening books. */ +#cmakedefine PENTOBI_BOOKS_DIR "@PENTOBI_BOOKS_DIR@" + +/** Location of the Pentobi user manual. */ +#cmakedefine PENTOBI_HELP_DIR "@PENTOBI_HELP_DIR@" + +/** Location of the translations directory. */ +#cmakedefine PENTOBI_TRANSLATIONS "@PENTOBI_TRANSLATIONS@" diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt new file mode 100644 index 0000000..9fef947 --- /dev/null +++ b/data/CMakeLists.txt @@ -0,0 +1,51 @@ +if(PENTOBI_BUILD_GUI AND NOT WIN32) + +add_custom_target( + pentobi-64.png ALL + COMMAND convert ${CMAKE_SOURCE_DIR}/src/pentobi/icons/pentobi-64.svg pentobi-64.png + DEPENDS ${CMAKE_SOURCE_DIR}/src/pentobi/icons/pentobi-64.svg + ) +add_custom_target( + application-x-blokus-sgf.png ALL + COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf.svg application-x-blokus-sgf.png + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf.svg + ) +add_custom_target( + application-x-blokus-sgf-16.png ALL + COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf-16.svg application-x-blokus-sgf-16.png + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf-16.svg + ) + +configure_file(pentobi.desktop.in pentobi.desktop @ONLY) +configure_file(pentobi.thumbnailer.in pentobi.thumbnailer @ONLY) +configure_file(pentobi.appdata.xml.in pentobi.appdata.xml @ONLY) +install(FILES ${CMAKE_BINARY_DIR}/src/pentobi/icons/pentobi.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps) +install(FILES ${CMAKE_BINARY_DIR}/src/pentobi/icons/pentobi-16.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps + RENAME pentobi.png) +install(FILES ${CMAKE_BINARY_DIR}/src/pentobi/icons/pentobi-32.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps + RENAME pentobi.png) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi-64.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps + RENAME pentobi.png) +install(FILES ${CMAKE_SOURCE_DIR}/src/pentobi/icons/pentobi.svg + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/mimetypes) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf-16.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/mimetypes + RENAME application-x-blokus-sgf.png) +install(FILES application-x-blokus-sgf.svg + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/mimetypes) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.desktop + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.thumbnailer + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/thumbnailers) +install(FILES pentobi-mime.xml + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mime/packages) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.appdata.xml + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/appdata) + +endif(PENTOBI_BUILD_GUI AND NOT WIN32) diff --git a/data/application-x-blokus-sgf-16.svg b/data/application-x-blokus-sgf-16.svg new file mode 100644 index 0000000..d0d9743 --- /dev/null +++ b/data/application-x-blokus-sgf-16.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/data/application-x-blokus-sgf-32.svg b/data/application-x-blokus-sgf-32.svg new file mode 100644 index 0000000..935183a --- /dev/null +++ b/data/application-x-blokus-sgf-32.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/application-x-blokus-sgf-64.svg b/data/application-x-blokus-sgf-64.svg new file mode 100644 index 0000000..495a843 --- /dev/null +++ b/data/application-x-blokus-sgf-64.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/application-x-blokus-sgf.svg b/data/application-x-blokus-sgf.svg new file mode 100644 index 0000000..c903262 --- /dev/null +++ b/data/application-x-blokus-sgf.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/pentobi-mime.xml b/data/pentobi-mime.xml new file mode 100644 index 0000000..fe420d7 --- /dev/null +++ b/data/pentobi-mime.xml @@ -0,0 +1,23 @@ + + + +Blokus game +Blokus-Partie + + + + + + + + + + + + + + + + + + diff --git a/data/pentobi.appdata.xml.in b/data/pentobi.appdata.xml.in new file mode 100644 index 0000000..c3c691b --- /dev/null +++ b/data/pentobi.appdata.xml.in @@ -0,0 +1,91 @@ + + + pentobi.desktop + CC0-1.0 + GPL-3.0+ + Pentobi + Pentobi + Computer opponent for the board game Blokus + Computer-Gegner für das Brettspiel Blokus + + +

Pentobi is a computer opponent for the board game Blokus. It has a + strong Blokus engine with 9 different playing levels. The supported game + variants are: Classic, Duo, Trigon, Junior, Nexos, Callisto.

+

Pentobi ist ein Computer-Gegner für das Brettspiel Blokus. + Es hat eine spielstarke Blokus-Engine mit 9 verschiedenen Spielstufen. + Die unterstützten Spielvarianten sind : Klassisch, Duo, Trigon, Junior, + Nexos, Callisto.

+ +

Players can determine their strength by playing rated games against the + computer and use a game analysis function. Games can be saved in Smart Game + Format with comments and move variations.

+

Spieler können ihre Spielstärke ermitteln, indem sie + gewertete Spiele gegen den Computer spielen, und eine Spielanalysefunktion + benutzen. Spiele können im Smart-Game-Format gespeichert werden mit + Kommentaren und Zugvarianten.

+ +

System requirements: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2 GHz dual-core or + faster CPU recommended for playing level 9).

+

Systemminima: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2 GHz + Dual-Core- oder schnellere CPU empfohlen für Spielstufe 9).

+ +

Trademark disclaimer: The trademark Blokus and other trademarks referred + to are property of their respective trademark holders. The trademark + holders are not affiliated with the author of the program Pentobi.

+

Hinweis zu Markennamen: Der Markenname Blokus und andere + erwähnte Marken sind Eigentum ihrer jeweiligen Markeninhaber. Die + Markeninhaber stehen in keiner Verbindung mit dem Autor des Programms + Pentobi.

+
+ + + + + http://pentobi.sourceforge.net/pentobi-classic.png + Game variant Classic + Spielvariante Klassisch + + + + http://pentobi.sourceforge.net/pentobi-duo.png + Game variant Duo + Spielvariante Duo + + + + http://pentobi.sourceforge.net/pentobi-trigon.png + Game variant Trigon + Spielvariante Trigon + + + + http://pentobi.sourceforge.net/pentobi-nexos.png + Game variant Nexos + Spielvariante Nexos + + + + http://pentobi.sourceforge.net/pentobi-callisto.png + Game variant Callisto + Spielvariante Callisto + + + + http://pentobi.sourceforge.net/ + https://sourceforge.net/p/pentobi/bugs/ + https://sourceforge.net/p/pentobi/donate/ + Markus Enzenberger + enz@users.sourceforge.net + + + pentobi + + + application/x-blokus-sgf + + + + + +
diff --git a/data/pentobi.desktop.in b/data/pentobi.desktop.in new file mode 100755 index 0000000..0992025 --- /dev/null +++ b/data/pentobi.desktop.in @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Pentobi +GenericName=Computer Opponent for Blokus +GenericName[de]=Computer-Gegner für Blokus +Comment=Computer opponent for the board game Blokus +Comment[de]=Computer-Gegner für das Brettspiel Blokus +Keywords=Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto; +Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi %f +Icon=pentobi +Type=Application +Categories=Game;BoardGame; +MimeType=application/x-blokus-sgf; diff --git a/data/pentobi.thumbnailer.in b/data/pentobi.thumbnailer.in new file mode 100644 index 0000000..dd95f90 --- /dev/null +++ b/data/pentobi.thumbnailer.in @@ -0,0 +1,3 @@ +[Thumbnailer Entry] +Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi-thumbnailer --size %s %i %o +MimeType=application/x-blokus-sgf; diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt new file mode 100644 index 0000000..5cf3005 --- /dev/null +++ b/doc/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(man) diff --git a/doc/blksgf/Pentobi-SGF.html b/doc/blksgf/Pentobi-SGF.html new file mode 100644 index 0000000..76b40a9 --- /dev/null +++ b/doc/blksgf/Pentobi-SGF.html @@ -0,0 +1,147 @@ + + + +Pentobi SGF Files + + + +

Pentobi SGF Files

+
Author: Markus Enzenberger
+Last modified: 2016-11-27
+

This document describes the file format for Blokus game records as used by the +program Pentobi. The most recent +version of this document can be found in the source code distribution of +Pentobi in the folder pentobi/doc/blksgf.

+

Introduction

+

The file format is a derivative of the Smart Game Format (SGF). The current SGF +version 4 does not define standard properties for Blokus. Therefore, a number +of game-specific properties and value types had to be defined. The definitions +follow the recommendations of SGF 4 and the proposals for multi-player games +from the discussions +about the future SGF version 5.

+

File Extension and MIME Type

+

The file extension .blksgf and the MIME type +application/x-blokus-sgf are used for Blokus SGF files.

+

Note
+Since this is a non-standard MIME type, links to Blokus SGF files on web +servers will not automatically open the file with Pentobi even if Pentobi is +installed locally and registered as a handler for Blokus SGF files. To make +this work, you can put a file named .htaccess on the web server in the +same directory that contains the .blksgf files or in one of its parent +directories. This file needs to contain the line:

+
AddType application/x-blokus-sgf +blksgf
+

Character Set

+

Although not specific to Blokus, it is recommended to use UTF-8 as the character set. Pentobi +always writes files in UTF-8 and indicates that with the CA property. +Pentobi can read SGF files encoded in UTF-8 or ISO-8859-1 (Latin1). Other +character sets are currently not supported. As specified by the SGF standard, +ISO-8859-1 is assumed for files without CA property.

+

Game Property

+

Since there is no number for Blokus defined in SGF 4, a string instead of a +number is used as the value for the GM property. Currently, the +following strings are used: Blokus, Blokus Two-Player, Blokus Three-Player, +Blokus Duo, Blokus Trigon, Blokus Trigon Two-Player, Blokus Trigon +Three-Player, Blokus Junior, Nexos, Nexos Two-Player, Callisto, Callisto +Two-Player, Callisto Three-Player.

+The strings are case-sensitive, words are separated by exactly one space and no +additional whitespace at the beginning or end of the string is allowed. +

Color and Player Properties

+

In game variants with two players and two colors, B denotes the +first player or color, W the second player or color. In game variants +with three or four players and one color per player, 1, 2, +3, 4 denote the first, second, third, and fourth player or +color. In game variants with two players and four colors, B denotes +the first player, W the second player, and 1, 2, +3, 4 denote the first, second, third, and fourth color. This +applies to move properties and properties related to a player or a color.

+

Example 1: in the game variant Blokus Two-Player PW is the name of +the first player, and 1 is a move of the first color.

+

Example 2: in the game variant Blokus Two-Player, one could either use the +BL, WL properties to indicate the time left for a player, if +the game is played with a time limit for each player, or one could use the +1L, 2L, 3L, 4L properties to indicate the +time left for a color, if the game is played with a time limit for each color. +(This is only an example how the properties should be interpreted. Pentobi +currently has no support for game clocks.)

+

Note
+Pentobi versions before 0.2 used the properties BLUE, YELLOW, +RED, GREEN in the four-color game variants, which did not +reflect the current state of discussion for SGF 5. Pentobi 12.0 erroneously +used multi-player properties for two-player Callisto. Current versions of +Pentobi can still read games written by older versions and will convert old +properties.

+

Coordinate System

+

Fields on the board (called points in SGF) are identified by a +case-insensitive string with a letter for the column followed by a number for +the row. The letters start with 'a', the numbers start with '1'. The lower left +corner of the board is 'a1'. The strings are not allowed to contain +whitespaces. Note that, unlike the common convention in the game of Go, the +letter 'i' is used.

+

If there are more than 26 columns, the columns continue with 'aa', 'ab', +..., 'ba', 'bb', ... More than 26 columns are presently required for Trigon and +could also be required for future game variants on rectangular boards larger +than 26×26.

+

For Trigon, hexagonal boards are mapped to rectangular coordinates as in the +following example of a hexagon with edge size 3:

+
+       6     / \ / \ / \ / \
+       5   / \ / \ / \ / \ / \
+       4 / \ / \ / \ / \ / \ / \
+       3 \ / \ / \ / \ / \ / \ /
+       2   \ / \ / \ / \ / \ /
+       1     \ / \ / \ / \ /
+          a b c d e f g h i j k
+
+

In Nexos, the 13×13 line grid is mapped to a 25×25 coordinate system, in +which rows with horizontal line segments and intersections alternate with rows +with vertical line segments and holes:

+
+       6 |   |   |
+       5 + - + - + -
+       4 |   |   |
+       3 + - + - + -
+       2 |   |   |
+       1 + - + - + -
+         a b c d e f
+
+

Move Properties

+

The value of a move property is a string with the coordinates of the played +piece on the board separated by commas. No whitespace characters are allowed +before, after, or in-between the coordinates.

+

Pentobi currently does not require a certain order of the coordinates of a +move. However, move properties should be written with an ordered list of +coordinates (using the order a1, b1, …, a2, b2, …) such that each move has a +unique string representation.

+

Example: B[f9,e10,f10,g10,f11]

+

In Nexos, moves contain only the coordinates of line segments occupied by +the piece, no coordinates of junctions.

+

Note
+Old versions of Pentobi (before version 0.3) used to represent moves by a list +of points, which did not follow the convention used by other games in SGF to +use single-value properties for moves. Current versions of Pentobi can still +read games containing the old move property values but they are deprecated and +should no longer be used.

+

Setup Properties

+

The setup properties AB, AW, A1, A2, +A3, A4 can be used to place several pieces simultaneously on +the board. The setup property AE can be used to remove pieces from the +board. All these properties can have multiple values, each value represents a +piece by its coordinates as in the move properties. The PL can be used +to set the color to play in a setup position.

+

Example:
+AB[e8,e9,f9,d10,e10][g6,f7,g7,h7,g8]
+AW[i4,h5,i5,j5,i6][j7,j8,j9,k9,j10]
+PL[B]

+

Note
+Older versions of Pentobi (before version 2.0) did not support setup +properties, you need a newer version of Pentobi to read such files. Currently, +Pentobi is able to read files with setup properties in any node, but can create +only files with setup in the root node.

+ + diff --git a/doc/doxygen/.gitignore b/doc/doxygen/.gitignore new file mode 100644 index 0000000..1b0a5aa --- /dev/null +++ b/doc/doxygen/.gitignore @@ -0,0 +1,2 @@ +doxygen_sqlite3.db +html/ diff --git a/doc/doxygen/Doxyfile b/doc/doxygen/Doxyfile new file mode 100644 index 0000000..326986f --- /dev/null +++ b/doc/doxygen/Doxyfile @@ -0,0 +1,317 @@ +# Doxyfile 1.8.9.1 + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = Pentobi +PROJECT_NUMBER = +PROJECT_BRIEF = +PROJECT_LOGO = +OUTPUT_DIRECTORY = +CREATE_SUBDIRS = NO +ALLOW_UNICODE_NAMES = NO +OUTPUT_LANGUAGE = English +BRIEF_MEMBER_DESC = YES +REPEAT_BRIEF = YES +ABBREVIATE_BRIEF = +ALWAYS_DETAILED_SEC = NO +INLINE_INHERITED_MEMB = NO +FULL_PATH_NAMES = YES +STRIP_FROM_PATH = +STRIP_FROM_INC_PATH = +SHORT_NAMES = NO +JAVADOC_AUTOBRIEF = YES +QT_AUTOBRIEF = NO +MULTILINE_CPP_IS_BRIEF = NO +INHERIT_DOCS = YES +SEPARATE_MEMBER_PAGES = NO +TAB_SIZE = 8 +ALIASES = +TCL_SUBST = +OPTIMIZE_OUTPUT_FOR_C = NO +OPTIMIZE_OUTPUT_JAVA = NO +OPTIMIZE_FOR_FORTRAN = NO +OPTIMIZE_OUTPUT_VHDL = NO +EXTENSION_MAPPING = +MARKDOWN_SUPPORT = YES +AUTOLINK_SUPPORT = YES +BUILTIN_STL_SUPPORT = NO +CPP_CLI_SUPPORT = NO +SIP_SUPPORT = NO +IDL_PROPERTY_SUPPORT = YES +DISTRIBUTE_GROUP_DOC = NO +SUBGROUPING = YES +INLINE_GROUPED_CLASSES = NO +INLINE_SIMPLE_STRUCTS = NO +TYPEDEF_HIDES_STRUCT = NO +LOOKUP_CACHE_SIZE = 0 +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- +EXTRACT_ALL = YES +EXTRACT_PRIVATE = NO +EXTRACT_PACKAGE = NO +EXTRACT_STATIC = NO +EXTRACT_LOCAL_CLASSES = YES +EXTRACT_LOCAL_METHODS = NO +EXTRACT_ANON_NSPACES = NO +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO +HIDE_FRIEND_COMPOUNDS = NO +HIDE_IN_BODY_DOCS = NO +INTERNAL_DOCS = NO +CASE_SENSE_NAMES = YES +HIDE_SCOPE_NAMES = NO +HIDE_COMPOUND_REFERENCE= NO +SHOW_INCLUDE_FILES = YES +SHOW_GROUPED_MEMB_INC = NO +FORCE_LOCAL_INCLUDES = NO +INLINE_INFO = YES +SORT_MEMBER_DOCS = YES +SORT_BRIEF_DOCS = NO +SORT_MEMBERS_CTORS_1ST = NO +SORT_GROUP_NAMES = NO +SORT_BY_SCOPE_NAME = NO +STRICT_PROTO_MATCHING = NO +GENERATE_TODOLIST = YES +GENERATE_TESTLIST = YES +GENERATE_BUGLIST = YES +GENERATE_DEPRECATEDLIST= YES +ENABLED_SECTIONS = +MAX_INITIALIZER_LINES = 30 +SHOW_USED_FILES = YES +SHOW_FILES = YES +SHOW_NAMESPACES = YES +FILE_VERSION_FILTER = +LAYOUT_FILE = +CITE_BIB_FILES = +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- +QUIET = NO +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_NO_PARAMDOC = NO +WARN_FORMAT = "$file:$line: $text" +WARN_LOGFILE = +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- +INPUT = ../../src +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = *.h \ + *.cpp +RECURSIVE = YES +EXCLUDE = +EXCLUDE_SYMLINKS = NO +EXCLUDE_PATTERNS = +EXCLUDE_SYMBOLS = +EXAMPLE_PATH = +EXAMPLE_PATTERNS = +EXAMPLE_RECURSIVE = NO +IMAGE_PATH = +INPUT_FILTER = +FILTER_PATTERNS = +FILTER_SOURCE_FILES = NO +FILTER_SOURCE_PATTERNS = +USE_MDFILE_AS_MAINPAGE = +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- +SOURCE_BROWSER = NO +INLINE_SOURCES = NO +STRIP_CODE_COMMENTS = YES +REFERENCED_BY_RELATION = NO +REFERENCES_RELATION = NO +REFERENCES_LINK_SOURCE = YES +SOURCE_TOOLTIPS = YES +USE_HTAGS = NO +VERBATIM_HEADERS = YES +CLANG_ASSISTED_PARSING = NO +CLANG_OPTIONS = +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- +ALPHABETICAL_INDEX = NO +COLS_IN_ALPHA_INDEX = 5 +IGNORE_PREFIX = +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- +GENERATE_HTML = YES +HTML_OUTPUT = html +HTML_FILE_EXTENSION = .html +HTML_HEADER = +HTML_FOOTER = footer.html +HTML_STYLESHEET = +HTML_EXTRA_STYLESHEET = +HTML_EXTRA_FILES = +HTML_COLORSTYLE_HUE = 220 +HTML_COLORSTYLE_SAT = 100 +HTML_COLORSTYLE_GAMMA = 80 +HTML_TIMESTAMP = YES +HTML_DYNAMIC_SECTIONS = NO +HTML_INDEX_NUM_ENTRIES = 100 +GENERATE_DOCSET = NO +DOCSET_FEEDNAME = "Doxygen generated docs" +DOCSET_BUNDLE_ID = org.doxygen.Project +DOCSET_PUBLISHER_ID = org.doxygen.Publisher +DOCSET_PUBLISHER_NAME = Publisher +GENERATE_HTMLHELP = NO +CHM_FILE = +HHC_LOCATION = +GENERATE_CHI = NO +CHM_INDEX_ENCODING = +BINARY_TOC = NO +TOC_EXPAND = NO +GENERATE_QHP = NO +QCH_FILE = +QHP_NAMESPACE = org.doxygen.Project +QHP_VIRTUAL_FOLDER = doc +QHP_CUST_FILTER_NAME = +QHP_CUST_FILTER_ATTRS = +QHP_SECT_FILTER_ATTRS = +QHG_LOCATION = +GENERATE_ECLIPSEHELP = NO +ECLIPSE_DOC_ID = org.doxygen.Project +DISABLE_INDEX = NO +GENERATE_TREEVIEW = NO +ENUM_VALUES_PER_LINE = 4 +TREEVIEW_WIDTH = 250 +EXT_LINKS_IN_WINDOW = NO +FORMULA_FONTSIZE = 10 +FORMULA_TRANSPARENT = YES +USE_MATHJAX = NO +MATHJAX_FORMAT = HTML-CSS +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest +MATHJAX_EXTENSIONS = +MATHJAX_CODEFILE = +SEARCHENGINE = NO +SERVER_BASED_SEARCH = NO +EXTERNAL_SEARCH = NO +SEARCHENGINE_URL = +SEARCHDATA_FILE = searchdata.xml +EXTERNAL_SEARCH_ID = +EXTRA_SEARCH_MAPPINGS = +#--------------------------------------------------------------------------- +# Configuration options related to the LaTeX output +#--------------------------------------------------------------------------- +GENERATE_LATEX = NO +LATEX_OUTPUT = latex +LATEX_CMD_NAME = latex +MAKEINDEX_CMD_NAME = makeindex +COMPACT_LATEX = NO +PAPER_TYPE = a4wide +EXTRA_PACKAGES = +LATEX_HEADER = +LATEX_FOOTER = +LATEX_EXTRA_STYLESHEET = +LATEX_EXTRA_FILES = +PDF_HYPERLINKS = YES +USE_PDFLATEX = YES +LATEX_BATCHMODE = NO +LATEX_HIDE_INDICES = NO +LATEX_SOURCE_CODE = NO +LATEX_BIB_STYLE = plain +#--------------------------------------------------------------------------- +# Configuration options related to the RTF output +#--------------------------------------------------------------------------- +GENERATE_RTF = NO +RTF_OUTPUT = rtf +COMPACT_RTF = NO +RTF_HYPERLINKS = NO +RTF_STYLESHEET_FILE = +RTF_EXTENSIONS_FILE = +RTF_SOURCE_CODE = NO +#--------------------------------------------------------------------------- +# Configuration options related to the man page output +#--------------------------------------------------------------------------- +GENERATE_MAN = NO +MAN_OUTPUT = man +MAN_EXTENSION = .3 +MAN_SUBDIR = +MAN_LINKS = NO +#--------------------------------------------------------------------------- +# Configuration options related to the XML output +#--------------------------------------------------------------------------- +GENERATE_XML = NO +XML_OUTPUT = xml +XML_PROGRAMLISTING = YES +#--------------------------------------------------------------------------- +# Configuration options related to the DOCBOOK output +#--------------------------------------------------------------------------- +GENERATE_DOCBOOK = NO +DOCBOOK_OUTPUT = docbook +DOCBOOK_PROGRAMLISTING = NO +#--------------------------------------------------------------------------- +# Configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- +GENERATE_AUTOGEN_DEF = NO +#--------------------------------------------------------------------------- +# Configuration options related to the Perl module output +#--------------------------------------------------------------------------- +GENERATE_PERLMOD = NO +PERLMOD_LATEX = NO +PERLMOD_PRETTY = YES +PERLMOD_MAKEVAR_PREFIX = +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = NO +EXPAND_ONLY_PREDEF = NO +SEARCH_INCLUDES = YES +INCLUDE_PATH = +INCLUDE_FILE_PATTERNS = +PREDEFINED = HAVE_BOOST_THREAD +EXPAND_AS_DEFINED = +SKIP_FUNCTION_MACROS = YES +#--------------------------------------------------------------------------- +# Configuration options related to external references +#--------------------------------------------------------------------------- +TAGFILES = +GENERATE_TAGFILE = +ALLEXTERNALS = NO +EXTERNAL_GROUPS = YES +EXTERNAL_PAGES = YES +PERL_PATH = /usr/bin/perl +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- +CLASS_DIAGRAMS = YES +MSCGEN_PATH = +DIA_PATH = +HIDE_UNDOC_RELATIONS = YES +HAVE_DOT = NO +DOT_NUM_THREADS = 0 +DOT_FONTNAME = Helvetica +DOT_FONTSIZE = 10 +DOT_FONTPATH = +CLASS_GRAPH = YES +COLLABORATION_GRAPH = YES +GROUP_GRAPHS = YES +UML_LOOK = NO +UML_LIMIT_NUM_FIELDS = 10 +TEMPLATE_RELATIONS = NO +INCLUDE_GRAPH = YES +INCLUDED_BY_GRAPH = YES +CALL_GRAPH = NO +CALLER_GRAPH = NO +GRAPHICAL_HIERARCHY = YES +DIRECTORY_GRAPH = YES +DOT_IMAGE_FORMAT = png +INTERACTIVE_SVG = NO +DOT_PATH = +DOTFILE_DIRS = +MSCFILE_DIRS = +DIAFILE_DIRS = +PLANTUML_JAR_PATH = +PLANTUML_INCLUDE_PATH = +DOT_GRAPH_MAX_NODES = 50 +MAX_DOT_GRAPH_DEPTH = 0 +DOT_TRANSPARENT = YES +DOT_MULTI_TARGETS = NO +GENERATE_LEGEND = YES +DOT_CLEANUP = YES diff --git a/doc/doxygen/footer.html b/doc/doxygen/footer.html new file mode 100644 index 0000000..13af170 --- /dev/null +++ b/doc/doxygen/footer.html @@ -0,0 +1,8 @@ +

+


+
+$date Doxygen $doxygenversion +
+

+ + diff --git a/doc/gtp/Pentobi-GTP.html b/doc/gtp/Pentobi-GTP.html new file mode 100644 index 0000000..6864272 --- /dev/null +++ b/doc/gtp/Pentobi-GTP.html @@ -0,0 +1,321 @@ + + + +Pentobi GTP Interface + + + +

Pentobi GTP Interface

+
Author: Markus Enzenberger
+

This document describes the text-based interface to the engine of the Blokus +program Pentobi. The interface is +an adaption of the Go Text +Protocol (GTP) and allows controller programs to use the engine in an +automated way without the GUI. The most recent version of this document can be +found in the source code distribution of Pentobi in the folder +pentobi/doc/gtp.

+

Go Text Protocol

+

The Go Text Protocol is a simple text-based protocol. The engine reads +single-line commands from its standard input stream and writes multi-line +responses to its standard output stream. The first character of a response is a +status character: = for success, ? for failure, followed by +the actual response. The response ends with two consecutive newline characters. +See the GTP +specification for details.

+

Controllers

+

To use the engine from a controller program, the controller typically +creates a child process by running pentobi-gtp and then sends commands +and receives responses through the input/output streams of the child process. +How this is done, depends on the platform (programming language and/or +operating system). In Java, for example, a child process can be created with +java.lang.Runtime.exec().

+

Note that the input/output streams of child processes are often fully +buffered. You should explicitly flush the output stream after sending a +command. Another caveat is that pentobi-gtp writes debugging +information to its standard error stream. On some platforms the standard error +stream of the child process is automatically connected to the standard error +stream of its parent process. If not (this happens for example in Java), the +controller needs to read everything from the standard error stream of the child +process. This can be done for example by running a separate thread in the +parent process that has a simple read loop, which writes everything that it +reads to its own standard error stream or discards it. Otherwise the child +process will block as soon as the buffer for its standard error stream is full. +Alternatively, you can disable debugging output of pentobi-gtp with +the command line option --quiet, but it is generally better to assume +that a GTP engine writes text to standard error.

+

An example for a controller written in C++ for Linux is included in Pentobi +since version 9.0 in src/twogtp. The controller starts two GTP engines +and plays a number of Blokus games between them. Older versions of Pentobi +included a Python script with a similar functionality in +tools/twogtp/twogtp.py.

+

Building

+

Since the GTP engine is a developer tool, building it is not enabled by +default. To enable it, run cmake with the option +-DPENTOBI_BUILD_GTP=ON. After building, there will be an executable in +the build directory named src/pentobi_gtp/pentobi-gtp. The GTP engine +requires only standard C++ and has no dependency on other libraries like Qt, +which is needed for the GUI version of Pentobi. If you only want to build the +GTP engine, you can disable building the GUI with +-DPENTOBI_BUILD_GUI=OFF.

+

Options

+

The following command-line options are supported by +pentobi-gtp:

+
+
--book file
+
Specify a file name for the opening book. Opening books are blksgf files +containing trees, in which moves that Pentobi should select are marked as good +moves with the corresponding SGF property (see the files in +src/books). If no opening book is specified and opening books are not +disabled, pentobi-gtp will automatically search for an opening book +for the current game variant in the directory of the executable using the same +file name conventions as in src/books. If no such file is found it +will print an error message to standard error and disable the use of opening +books.
+
--config,-c file
+
Load a file with GTP commands and execute them before starting the main +loop, which reads commands from standard input. This can be used for +configuration files that contain GTP commands for setting parameters of the +engine (see below).
+
--color
+
Use ANSI escape sequences to colorize the text output of boards (for +example in the response to the showboard command or with the +--showboard command line option).
+
--cputime
+
Use CPU time instead of wall time for time measurement. Currently, there is +no way to make Pentobi play with time limits, the levels are defined by the +number of simulations in the MCTS search, so this affects only the debugging +output, which prints the time used after each search.
+
--game,-g variant
+
Specify the game variant used at start-up. Valid arguments are classic, +classic_2, duo, trigon, trigon_2, trigon_3, junior or the abbreviations c, c2, +d, t, t2, t3, j. By default, the initial game variant is classic. The game +variant can also be changed at run-time with a GTP command. If only a single +game variant is used, it is slightly faster and saves memory if the engine is +started in the right variant compared to having it start with classic and then +changing it.
+
--help,-h
+
Print a list of the command-line options and exit.
+
--level,-l n
+
Set the level of playing strength to n. Valid values are 1 to 9.
+
--seed,-r n
+
Use n as the seed for the random generator. Specifying a random seed +will make the move generation deterministic as long as the search is +single-threaded.
+
--showboard
+
Automatically write a text representation of the current position to +standard error after each command that alters the position.
+
--nobook
+
Disable the use of opening books.
+
--noresign
+
Disable resignation. If resignation is desabled, the genmove +command will never respond with resign. Resignation can speed up the +playing of test games if only the win/loss information is wanted.
+
--quiet,-q
+
Do not print any debugging messages, errors or warnings to standard +error.
+
--threads n
+
Use n threads during the search. Note that the default is 1, unlike +in the GUI version of Pentobi, which sets the default according to the number +of hardware threads (CPUs, cores or virtual cores) available on the current +system. The reason is that, for example, using 2 threads makes the search twice +as fast but may lose a bit of playing strength compared to the single-threaded +search. Therefore, if the GTP engine is used to play many test games with +twogtp.py (which supports playing games in parallel), it is better to play the +games with single-threaded search in parallel than with multi-threaded search +sequentially. Using a large number of threads (e.g. more than 4 on game +variants with a small board or more than 8 on large boards) is untested and +might reduce the playing strength significantly compared to the single-threaded +search.
+
--version,-v
+
Print the version of Pentobi and exit.
+
+

Commands

+

Standard Commands

+

The following GTP commands have the same or an equivalent meaning as +specified by the GTP standard. Colors or players in arguments or responses are +represented as in the property IDs of blksgf files (B, W if +two players or colors; 1, 2, 3, 4 if more +than two). Moves in arguments or responses are represented as in the move +property values of blksgf files. See the specification for Pentobi SGF files for +details.

+
+
all_legal color
+
List all legal moves for a color.
+
clear_board
+
Clear the board and start a new game in the current game variant.
+
final_score
+
Get the score of a final board position. In two-player game variants, the +format of the response is as in the result property in the SGF standard for the +game of Go (e.g. B+2 if the first player wins with two points, or +0 for a draw). In game variants with more than two players, the +response is a list of the points for each player (e.g. +64 69 70 40). If the current position is not a final +position, the response is undefined.
+
genmove color
+
Generate and play a move for a given color in the current position. If the +color has no more moves, the response is pass. If resignation is not +disabled, the response is resign if the players is very likely to +lose. Otherwise the response is the move.
+
known_command command
+
The response is true if command is a GTP command supported +by the engine, false otherwise.
+
list_commands
+
List all supported GTP commands, one command per line.
+
loadsgf file [move_number]
+
Load a board position from a blksgf file with name file. If +move_number is specified, the board position will be set to the position +in the main variation of the file before the move with the given number +was played, otherwise to the last position in the main variation.
+
name
+
Return the name of the GTP engine (Pentobi).
+
play color move
+
Play a move for a given color in the current board position.
+
protocol_version
+
Return the version of the GTP protocol used (currently 2).
+
quit
+
Exit the command loop and quit the engine.
+
reg_genmove color
+
Like the genmove command, but only generates a move and does not +play it on the board.
+
showboard
+
Return a text representation of the current board position.
+
undo
+
Undo the last move played.
+
version
+
Return the version of Pentobi.
+
+

Generally Useful Extension Commands

+
+
cputime
+
Return the CPU time used by the engine since the start of the program.
+
cputime_diff
+
Return the CPU time used by the engine since the last call of cputime_diff +or start of the program if cputime_diff has not been called yet.
+
g
+
Shortcut for the genmove command with the color argument set to +the current color to play.
+
get_place color
+
Get the place of a given color in the list of scores in a final position +(e.g. in game variant Classic, 1 is the place with the highest score, 4 the one +with the lowest, if all players have a different score). If some colors have +the same score, they share the same place and the string shared is +appended to the place number.
+
get_value
+
Get an estimated value of the board position from the view point of the +color of the last generated move. The return value is a win/loss estimation +between 0 (loss) and 1 (win) as produced by the last search performed by the +engine. This command should only be used immediately after a +reg_genmove or genmove command, otherwise the result is +undefined. The value is not very meaningful at the lowest playing levels. Note +that no searches are performed if the opening book is used for a move +generation and there is currently no way to check if this was so. Therefore, +the opening book should be disabled if the get_value command is +used.
+
p move
+
Shortcut for the play command with the color argument set to the +current color to play.
+
param [key value]
+
Set or query parameters specific to the Pentobi engine that can be changed +at run-time. If no arguments are given, the response is a list of the current +value with one key/value pair per line, otherwise the parameter with the given +key will be set to the given value. Generally useful parameters are: +
+
+
avoid_symmetric_draw 0|1
+
In some game variants (Duo, Trigon_2), the second player can enforce a tie +by answering each move by its symmetric counterpart if the first players misses +the opportunity to break the symmetry in the center. Technically, exploiting +this mistake by the first player is a good strategy for the second player +because a draw is a good result considering the first-play advantage. However, +playing symmetrically could be considered bad style, so this behavior is +avoided (value 1) by default.
+
fixed_simulations n
+
Use exactly n MCTS simulations during a search. By default, the +search engine uses levels, which determine how many MCTS simulations are run +during a search, but as a function that increases with the move number (because +the simulations become much faster at the end of the game). For some +experiments, it can be desirable to use a fixed number of simulations for each +move. If this number is specified, the playing level is ignored.
+
use_book 0|1
+
Enable or disable the opening book.
+
+
+The other parameters are only interesting for developers.
+
param_base [key value]
+
Set or query basic parameters that are not specific to the Pentobi engine. +If no arguments are given, the response is a list of the current value with one +key/value pair per line, otherwise the parameter with the given key will be set +to the given value. +
+
+
accept_illegal 0|1
+
Accept move arguments to the play command that violate the rules +of the game. If disabled, the play command will respond with an error, +otherwise it will perform the moves.
+
resign 0|1
+
Allow the engine to respond with resign to the genmove +command.
+
+
+
+
set_game variant
+
Set the current game variant and clear the board. The argument is the name +of the game variant as in the game property value of blksgf files (e.g. +Blokus Duo, see the specification for Pentobi SGF files for +details).
+
set_random_seed n
+
Set the seed of the random generator to n. See the documentation for +the command-line option --seed.
+
+

Extension Commands for Developers

+The remaining commands are only interesting for developers. See Pentobi's +source code for details. +

Example

+

The following GTP session queries the engine name and version, plays and +generates a move in game variant Duo and shows the resulting board position. +Commands are printed in bold, responses in normal text.

+
+$ ./pentobi-gtp --quiet
+name
+= Pentobi
+
+version
+= 7.1
+
+set_game Blokus Duo
+=
+
+play b e8,d9,e9,f9,e10
+=
+
+genmove w
+= i4,h5,i5,j5,i6
+
+showboard
+=
+   A B C D E F G H I J K L M N
+14 . . . . . . . . . . . . . . 14  *Blue(X): 5
+13 . . . . . . . . . . . . . . 13  1 F L5 N P T5 U V5 W Y
+12 . . . . . . . . . . . . . . 12  Z5 I5 O T4 Z4 L4 I4 V3 I3 2
+11 . . . . . . . . . . . . . . 11
+10 . . . . X . . . . . . . . . 10  Green(O): 5
+ 9 . . . X X X . . . . . . . . 9   1 F L5 N P T5 U V5 W Y
+ 8 . . . . X . . . . . . . . . 8   Z5 I5 O T4 Z4 L4 I4 V3 I3 2
+ 7 . . . . . . . . . . . . . . 7
+ 6 . . . . . . . .>O . . . . . 6
+ 5 . . . . . . . O O O . . . . 5
+ 4 . . . . . . . . O . . . . . 4
+ 3 . . . . . . . . . . . . . . 3
+ 2 . . . . . . . . . . . . . . 2
+ 1 . . . . . . . . . . . . . . 1
+   A B C D E F G H I J K L M N
+
+quit
+=
+
+
+ + diff --git a/doc/man/CMakeLists.txt b/doc/man/CMakeLists.txt new file mode 100644 index 0000000..d85259e --- /dev/null +++ b/doc/man/CMakeLists.txt @@ -0,0 +1,6 @@ +configure_file(pentobi.6.in pentobi.6 @ONLY) +configure_file(pentobi-thumbnailer.6.in pentobi-thumbnailer.6 @ONLY) +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/pentobi.6 + ${CMAKE_CURRENT_BINARY_DIR}/pentobi-thumbnailer.6 + DESTINATION ${CMAKE_INSTALL_MANDIR}/man6) diff --git a/doc/man/pentobi-thumbnailer.6.in b/doc/man/pentobi-thumbnailer.6.in new file mode 100644 index 0000000..a82c26d --- /dev/null +++ b/doc/man/pentobi-thumbnailer.6.in @@ -0,0 +1,35 @@ +.TH PENTOBI-THUMBNAILER 6 "2013-06-21" "Pentobi @PENTOBI_VERSION@" "Pentobi command reference" + +.SH NAME +pentobi-thumbnailer \- thumbnailer for game records for the board game Blokus as used by the program Pentobi + +.SH SYNOPSIS +.B pentobi-thumbnailer +.RI [ options ] " input-file output-file" +.br + +.SH DESCRIPTION + +.B pentobi-thumbnailer +is part of the program Pentobi and intended to be used as a thumbnailer for +the Gnome desktop environment to generate previews of game files written by +Pentobi. + +The input file is a game file in Pentobi's SGF format as documented in +doc/blksgf/Pentobi-SGF.html in the Pentobi source package. +The output file is a thumbnail in PNG format. + +.SH OPTIONS +.TP +.B \-s, \-\-size +The size of the thumbnail. The default is 128. + +.SH EXIT STATUS +.TP +0 if the thumbnail generation succeeds, 1 on error. + +.SH SEE ALSO +.BR pentobi (6) + +.SH AUTHOR +Markus Enzenberger diff --git a/doc/man/pentobi.6.in b/doc/man/pentobi.6.in new file mode 100644 index 0000000..a7ce12b --- /dev/null +++ b/doc/man/pentobi.6.in @@ -0,0 +1,49 @@ +.TH PENTOBI 6 "2015-01-04" "Pentobi @PENTOBI_VERSION@" "Pentobi command reference" + +.SH NAME +pentobi \- computer opponent for the board game Blokus + +.SH SYNOPSIS +.B pentobi +.RI [ options ] " [file]" +.br + +.SH DESCRIPTION + +.B pentobi +is the command to invoke the program Pentobi, which is a graphical user +interface and computer opponent to play the board game Blokus. + +The command can take the name of a game file to open at startup as an optional +argument. +The game file is expected to be in Pentobi's SGF format as documented in +doc/blksgf/Pentobi-SGF.html in the Pentobi source package. + +.SH OPTIONS +.TP +.B \-\-maxlevel +Set the maximum playing level. Reducing this value reduces the amount +of memory used by the search, which can be useful to run Pentobi on systems +that have low memory or are too slow to use the highest levels. +By default, Pentobi currently allocates up to 2 GB (but not more than a third +of the physical memory available on the system). +Reducing the maximum level to 8 currently reduces this amount by a factor +of 6 and lower maximum levels even more. +.TP +.B \-\-threads +The number of threads to use in the search. By default, up to 4 threads are +used in the search depending on the number of hardware threads supported +by the current system. +Using more threads will speed up the move generation but using a very high +number of threads (e.g. more than 8) can degrade the playing strength +in higher playing levels. +.TP +.B \-\-verbose +Print internal information about the move generation and other debugging +information to standard error. + +.SH SEE ALSO +.BR pentobi-thumbnailer (6) + +.SH AUTHOR +Markus Enzenberger diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..4f2ccf2 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,40 @@ +add_subdirectory(books) +add_subdirectory(libboardgame_sys) +add_subdirectory(libboardgame_util) +add_subdirectory(libboardgame_sgf) +add_subdirectory(libboardgame_base) +add_subdirectory(libboardgame_mcts) +add_subdirectory(libpentobi_base) +add_subdirectory(libpentobi_mcts) + +if (PENTOBI_BUILD_GTP) + add_subdirectory(libboardgame_gtp) + add_subdirectory(pentobi_gtp) + if(HAVE_UNISTD_H AND NOT(WIN32)) + add_subdirectory(twogtp) + else() + message(STATUS "Not building twogtp, needs POSIX") + endif() +endif() + +if (PENTOBI_BUILD_TESTS) + add_subdirectory(libboardgame_test) + add_subdirectory(libboardgame_test_main) + add_subdirectory(unittest) +endif() + +if (PENTOBI_BUILD_GUI) + add_subdirectory(convert) + add_subdirectory(libpentobi_gui) + add_subdirectory(libpentobi_thumbnail) + add_subdirectory(pentobi_thumbnailer) + add_subdirectory(pentobi) + if(PENTOBI_BUILD_KDE_THUMBNAILER) + add_subdirectory(libpentobi_kde_thumbnailer) + add_subdirectory(pentobi_kde_thumbnailer) + endif() +endif() + +if (PENTOBI_BUILD_QML) + add_subdirectory(pentobi_qml) +endif() diff --git a/src/books/CMakeLists.txt b/src/books/CMakeLists.txt new file mode 100644 index 0000000..d440a5a --- /dev/null +++ b/src/books/CMakeLists.txt @@ -0,0 +1,17 @@ +# Install the opening book files. If you change the destination, you need to +# update the default for PENTOBI_BOOKS_DIR in the main CMakeLists.txt +install(FILES + book_callisto.blksgf + book_callisto_2.blksgf + book_callisto_3.blksgf + book_classic.blksgf + book_classic_2.blksgf + book_classic_3.blksgf + book_duo.blksgf + book_junior.blksgf + book_nexos.blksgf + book_nexos_2.blksgf + book_trigon.blksgf + book_trigon_2.blksgf + book_trigon_3.blksgf + DESTINATION ${CMAKE_INSTALL_DATADIR}/pentobi/books) diff --git a/src/books/book_callisto.blksgf b/src/books/book_callisto.blksgf new file mode 100644 index 0000000..c9bccbf --- /dev/null +++ b/src/books/book_callisto.blksgf @@ -0,0 +1,16 @@ +( +;GM[Callisto] +( + ;1[g11]TE[1] + ( + ;2[n10]TE[1] + ) + ( + ;2[n11]TE[1] + ) +) +( + ;1[h12]TE[1] + ;2[m9]TE[1] +) +) diff --git a/src/books/book_callisto_2.blksgf b/src/books/book_callisto_2.blksgf new file mode 100644 index 0000000..14f40fa --- /dev/null +++ b/src/books/book_callisto_2.blksgf @@ -0,0 +1,24 @@ +( +;GM[Callisto Two-Player] +( + ;B[e9]TE[1] + ( + ;W[l8]TE[1] + ) + ( + ;W[k7]TE[1] + ) +) +( + ;B[f10]TE[1] + ( + ;W[k7]TE[1] + ) + ( + ;W[j6]TE[1] + ) + ( + ;W[l8] + ) +) +) diff --git a/src/books/book_callisto_3.blksgf b/src/books/book_callisto_3.blksgf new file mode 100644 index 0000000..6109c33 --- /dev/null +++ b/src/books/book_callisto_3.blksgf @@ -0,0 +1,21 @@ +( +;GM[Callisto Three-Player] +( + ;1[g11]TE[1] + ( + ;2[n10]TE[1] + ) + ( + ;2[n11]TE[1] + ) +) +( + ;1[h12]TE[1] + ( + ;2[m9]TE[1] + ) + ( + ;2[l8]TE[1] + ) +) +) diff --git a/src/books/book_classic.blksgf b/src/books/book_classic.blksgf new file mode 100644 index 0000000..bf9ad9e --- /dev/null +++ b/src/books/book_classic.blksgf @@ -0,0 +1,14 @@ +( +;GM[Blokus] +( + ;1[a17,b17,a18,a19,a20]TE[1] + ;2[s17,t17,t18,t19,t20]TE[1] + ;3[t1,t2,t3,s4,t4]TE[1] +) +( + ;1[b17,a18,b18,a19,a20]TE[1] +) +( + ;1[a20,b20,c20,c19,c18]TE[1] +) +) diff --git a/src/books/book_classic_2.blksgf b/src/books/book_classic_2.blksgf new file mode 100644 index 0000000..debcbc7 --- /dev/null +++ b/src/books/book_classic_2.blksgf @@ -0,0 +1,891 @@ +( +;GM[Blokus Two-Player] +( + ;1[a17,b17,a18,a19,a20]TE[1] + ( + ;2[s17,t17,t18,t19,t20]TE[1] + ( + ;3[t1,t2,s3,t3,s4]TE[1] + ( + ;4[a1,b1,c1,d1,d2]TE[1] + ( + ;1[d14,e14,d15,c16,d16]TE[1] + ( + ;2[p14,q14,q15,q16,r16]TE[1] + ( + ;3[r5,p6,q6,r6,p7]TE[1] + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[h11,g12,h12,f13,g13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[2] + ;3[o8,m9,n9,o9,p9]TE[1] + ;4[h6,h7,i7,i8,j8]TE[1] + ;1[j9,k9,l9,i10,j10]TE[2] + ;2[k10,l10,m10,n10,m11]TE[1] + ;3[q10,q11,q12,p13,q13]TE[1] + ) + ( + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[e3,f3,f4,g4,g5]TE[1] + ;1[g11,f12,g12,h12,f13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[1] + ;3[n8,o8,n9,m10,n10]TE[1] + ;4[h6,h7,i7,j7,i8]TE[1] + ;1[k10,l10,i11,j11,k11]TE[1] + ;2[l12,k13,l13,m13,l14]TE[1] + ;3[p9,q9,q10,r10,q11]TE[1] + ;4[l7,k8,l8,m8,l9]TE[1] + ) + ) + ( + ;2[p14,p15,q15,q16,r16]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ( + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[h11,g12,h12,f13,g13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[1] + ;3[o8,m9,n9,o9,p9]TE[1] + ;4[h6,h7,i7,i8,j8] + ;1[j9,k9,l9,i10,j10]TE[2] + ;2[k10,l10,m10,n10,m11]TE[1] + ;3[q10,q11,q12,p13,q13]TE[2] + ) + ( + ;4[e3,f3,f4,g4,g5]TE[1] + ) + ) + ) + ( + ;1[e14,d15,e15,c16,d16]TE[1] + ) + ) + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ( + ;3[r5,p6,q6,r6,p7]TE[1] + ;4[d4,d5,e5,e6,f6]TE[1] + ( + ;1[h11,g12,h12,f13,g13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[2] + ;3[o8,m9,n9,o9,p9]TE[1] + ;4[h6,g7,h7,h8,i8]TE[1] + ( + ;1[j9,k9,l9,i10,j10]TE[2] + ) + ( + ;1[k9,l9,i10,j10,k10]TE[2] + ) + ) + ( + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[1] + ;3[o8,o9,n10,o10,p10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[i10,j10,k10,l10,m10]TE[1] + ) + ) + ( + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[d4,d5,e5,e6,f6]TE[1] + ;1[g11,f12,g12,h12,f13]TE[1] + ;2[m11,n11,n12,o12,o13]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[k10,l10,i11,j11,k11]TE[1] + ;2[i12,j12,k12,l12,j13]TE[1] + ;3[p11,p12,q12,r12,p13]TE[1] + ;4[k7,l7,m7,n7,o7]TE[1] + ) + ) + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ) + ( + ;4[a1,a2,a3,b3,c3]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ;4[d4,e4,e5,e6,f6]TE[1] + ;1[h11,g12,h12,f13,g13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[2] + ;3[o8,m9,n9,o9,p9]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ( + ;1[j9,k9,l9,i10,j10]TE[2] + ;2[k10,l10,m10,n10,m11]TE[1] + ;3[q10,q11,q12,p13,q13]TE[1] + ) + ( + ;1[k9,l9,i10,j10,k10]TE[2] + ;2[l10,m10,n10,m11]TE[1] + ;3[q10,q11,q12,p13,q13]TE[1] + ) + ) + ) + ( + ;3[t1,t2,t3,s4,t4]TE[1] + ( + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ( + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[m11,n11,n12,o12,o13]TE[1] + ;3[o8,o9,p9,n10,o10]TE[1] + ;4[h6,h7,i7,i8,j8]TE[1] + ;1[i10,j10,k10,l10,m10]TE[1] + ;2[r10,p11,q11,r11,q12]TE[1] + ;3[k7,k8,l8,l9,m9]TE[1] + ;4[l5,j6,k6,l6,m6]TE[1] + ) + ( + ;2[q14,p15,q15,q16,r16]TE[1] + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[e3,f3,f4,g4,g5]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[o12,n13,o13,p13,o14]TE[1] + ;3[o8,o9,n10,o10,p10]TE[1] + ;4[h6,h7,i7,j7,i8]TE[1] + ;1[i10,j10,k10,l10,m10]TE[1] + ;2[m11,n11,l12,m12]TE[1] + ;3[k7,k8,l8,m8,m9]TE[1] + ;4[h9,f10,g10,h10,f11]TE[1] + ) + ) + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[e14,c15,d15,e15,c16]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[d4,d5,e5,e6,f6]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ( + ;2[o11,p11,n12,o12,o13]TE[1] + ;3[o8,o9,n10,o10,p10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[i10,j10,k10,l10,m10]TE[1] + ) + ( + ;2[n11,o11,p11,o12,o13]TE[1] + ;3[n8,o8,n9,m10,n10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[i10,j10,k10,l10]TE[1] + ;2[l11,k12,l12,m12,m13]TE[1] + ;3[p9,q9,q10,r10,q11]TE[1] + ) + ) + ( + ;4[a1,a2,a3,b3,c3]TE[1] + ) + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[m11,n11,n12,o12,o13]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[i12,j12,k12,l12,k13]TE[1] + ;3[k7,k8,j9,k9,l9]TE[1] + ) + ( + ;4[a1,a2,a3,a4,b4]TE[1] + ;1[e14,c15,d15,e15,c16]TE[1] + ;2[p14,q14,q15,r15,r16]TE[1] + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[c5,d5,d6,d7,e7]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[n11,m12,n12,n13,o13]TE[1] + ;3[o8,o9,p9,n10,o10]TE[1] + ;4[f8,g8,h8,h9,i9]TE[1] + ;1[i10,j10,k10,l10,m10]TE[1] + ;2[i11,j11,k11,l11,j12]TE[1] + ) + ) + ) + ( + ;2[r18,r19,s19,t19,t20]TE[1] + ;3[t1,t2,t3,s4,t4]TE[1] + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[o15,p15,p16,q16,q17]TE[1] + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[m13,l14,m14,n14,m15]TE[1] + ;3[o8,m9,n9,o9,o10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[j9,k9,l9,i10,j10]TE[1] + ;2[m11,n11,o11,p11,n12]TE[1] + ;3[l5,k6,l6,l7,l8]TE[1] + ;4[j4,j5,k5,i6,j6]TE[1] + ) + ( + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[d14,e14,c15,d15,c16]TE[1] + ;2[o15,p15,p16,q16,q17]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[e3,e4,f4,f5,g5]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[m13,l14,m14,n14,m15]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[h6,i6,i7,j7,k7]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[l11,m11,n11,o11,n12]TE[1] + ;3[p11,q11,q12,r12,q13]TE[1] + ;4[m7,l8,m8,n8,l9]TE[1] + ) + ) + ( + ;2[r18,s18,s19,s20,t20]TE[1] + ;3[t1,t2,t3,s4,t4]TE[1] + ;4[a1,a2,b2,c2,c3]TE[1] + ( + ;1[e14,c15,d15,e15,c16]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[f8,f9,g9,h9,i9]TE[1] + ;1[e8,d9,e9,e10,e11]TE[1] + ;2[l9,m9,l10,l11,m11]TE[1] + ;3[p11,p12,o13,p13,o14]TE[1] + ;4[c7,b8,c8,c9,c10]TE[1] + ) + ( + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[d4,d5,e5,e6,f6]TE[1] + ;1[g11,g12,h12,f13,g13]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,n9,o9,m10,n10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[i12,j12,k12,l12,i13]TE[1] + ;3[l6,l7,k8,l8,l9]TE[1] + ;4[j4,j5,i6,j6,k6]TE[1] + ) + ) +) +( + ;1[d19,a20,b20,c20,d20]TE[1] + ;2[r18,s18,s19,s20,t20]TE[1] + ( + ;3[q1,r1,s1,t1,q2]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f16,g16,e17,f17,e18]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[p3,o4,p4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j13,h14,i14,j14,h15]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,l8]TE[1] + ;4[g7,f8,g8,h8,h9]TE[1] + ;1[l9,l10,k11,l11,k12]TE[1] + ;2[m8,m9,n9,o9,n10]TE[1] + ;3[k9,j10,k10,j11,j12]TE[1] + ;4[i10,i11,i12,h13,i13]TE[1] + ) + ( + ;3[t1,t2,t3,s4,t4]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[g16,e17,f17,g17,e18]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j13,i14,j14,h15,i15]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ) +) +( + ;1[b18,c18,b19,a20,b20]TE[1] + ( + ;2[r18,s18,s19,s20,t20]TE[1] + ( + ;3[s1,t1,s2,r3,s3]TE[1] + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ( + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ( + ;1[j13,i14,j14,h15,i15]TE[1] + ( + ;2[m11,m12,m13,n13,n14]TE[1] + ( + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ( + ;2[l8,m8,n8,l9,l10]TE[1] + ;3[j9,j10,j11,j12]TE[1] + ;4[h11,g12,h12,h13,h14]TE[1] + ) + ( + ;2[l8,l9,m9,l10]TE[1] + ;3[i9,j9,j10,j11,j12]TE[1] + ;4[h11,g12,h12,h13,h14]TE[1] + ) + ) + ( + ;3[l5,k6,l6,m6,k7]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k8,k9,k10,k11,k12]TE[1] + ;2[l7,l8,m8,l9,l10]TE[1] + ;3[j8,j9,j10,j11,j12]TE[1] + ;4[h11,i11,h12,i12,i13]TE[1] + ) + ) + ( + ;2[k13,l13,m13,m14,n14]TE[1] + ( + ;3[l5,k6,l6,m6,k7]TE[1] + ( + ;4[f8,e9,f9,g9,g10]TE[1] + ;1[k8,k9,k10,k11,k12]TE[1] + ;2[o10,m11,n11,o11,n12]TE[1] + ;3[j8,j9,j10,j11,j12]TE[1] + ;4[h11,g12,h12,h13,h14]TE[1] + ) + ( + ;4[h7,f8,g8,h8,h9]TE[1] + ;1[k8,k9,k10,k11,k12]TE[1] + ;2[n9,n10,o10,n11,n12]TE[1] + ;3[j8,j9,j10,j11,j12]TE[1] + ;4[i10,i11,h12,i12,i13]TE[1] + ) + ) + ( + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ;2[i9,j9,j10,j11,j12]TE[1] + ;3[j4,i5,j5,k5,j6]TE[1] + ;4[f4,f5,g5,g6,h6]TE[1] + ) + ) + ) + ( + ;1[j14,h15,i15,j15,j16]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k9,k10,k11,k12,k13]TE[1] + ;2[l8,m8,l9,m9,l10]TE[1] + ;3[j9,j10,j11,j12,j13]TE[1] + ;4[h11,h12,g13,h13,h14]TE[1] + ) + ( + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[k13,l13,m13,m14,n14]TE[1] + ;3[m6,l7,m7,n7,m8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[i11,h12,i12,j12,i13]TE[1] + ;2[n10,m11,n11,o11,n12]TE[1] + ;3[n9,o9,o10,p10,p11]TE[1] + ;4[k9,i10,j10,k10,k11]TE[1] + ) + ) + ( + ;3[n4,o4,p4,q4,n5]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[j13,i14,j14,h15,i15]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l5,k6,l6,m6,k7]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k8,k9,k10,k11,k12]TE[1] + ;2[l7,l8,m8,l9,l10]TE[1] + ;3[j8,j9,j10,j11,j12]TE[1] + ;4[h11,h12,g13,h13,h14]TE[1] + ) + ) + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ( + ;1[j14,h15,i15,j15,j16]TE[1] + ;2[k13,l13,l14,m14,n14]TE[1] + ;3[l6,m6,k7,l7,l8]TE[1] + ;4[g7,h7,h8,i8,h9]TE[1] + ;1[j11,i12,j12,h13,i13]TE[1] + ;2[m10,l11,m11,m12,n12]TE[1] + ;3[h5,g6,h6,i6,j6]TE[1] + ;4[g10,e11,f11,g11,g12]TE[1] + ) + ( + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[k13,l13,m13,m14,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[j11,i12,j12,h13,i13]TE[1] + ;2[n9,n10,n11,o11,n12]TE[1] + ;3[h5,g6,h6,i6,j6]TE[1] + ;4[i9,j9,k9,l9,m9]TE[1] + ) + ( + ;1[j13,i14,j14,h15,i15]TE[1] + ( + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ( + ;4[g7,g8,h8,h9,h10]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ;2[l8,l9,m9,l10]TE[1] + ;3[j9,j10,i11,j11,j12]TE[1] + ;4[g11,f12,g12,h12,h13]TE[1] + ) + ( + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ;2[l8,l9,m9,l10]TE[1] + ;3[j9,j10,j11,j12]TE[1] + ;4[h10,h11,g12,h12,h13]TE[1] + ) + ) + ( + ;2[k13,l13,l14,m14,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,k10,k11,l11,k12]TE[1] + ;2[j9,j10,i11,j11,j12]TE[1] + ;3[h5,g6,h6,i6,j6]TE[1] + ;4[g4,h4,i4,i5,j5]TE[1] + ) + ) + ) + ( + ;4[a1,a2,a3,b3,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,e4,e5,e6,f6]TE[1] + ;1[j13,i14,j14,h15,i15]TE[1] + ;2[k13,l13,l14,m14,n14]TE[1] + ;3[m6,l7,m7,n7,m8]TE[1] + ;4[h6,g7,h7,i7,i8]TE[1] + ;1[i10,h11,i11,j11,i12]TE[1] + ;2[l10,k11,l11,m11,m12]TE[1] + ;3[n9,n10,o10,p10,o11]TE[1] + ;4[l5,m5,j6,k6,l6]TE[1] + ) + ) + ( + ;3[t1,t2,t3,s4,t4]TE[1] + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f15,e16,f16,d17,e17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ( + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[g7,g8,h8,i8,h9]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[i12,j12,k12,l12,i13]TE[1] + ;3[p11,p12,o13,p13,o14]TE[1] + ;4[m8,j9,k9,l9,m9]TE[1] + ) + ( + ;2[o12,m13,n13,o13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[i12,j12,k12,l12,i13]TE[1] + ;3[p11,p12,q12,r12,q13]TE[1] + ;4[k7,l7,m7,n7,o7]TE[1] + ) + ) + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[f15,e16,f16,d17,e17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[q5,r5,p6,q6,p7]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[f8,f9,g9,h9,f10]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[p8,p9,q9,p10,p11]TE[1] + ;3[k11,l11,i12,j12,k12]TE[1] + ;4[e11,e12,e13,e14,f14]TE[1] + ) + ) + ( + ;3[q1,r1,s1,t1,q2]TE[1] + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ( + ;3[p3,o4,p4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[n12,m13,n13,o13,n14]TE[1] + ;3[m6,k7,l7,m7,k8]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[k9,k10,k11,k12,k13]TE[1] + ;2[m8,m9,m10,l11,m11]TE[1] + ;3[j9,j10,j11,j12,j13]TE[1] + ;4[j5,i6,j6,k6,l6]TE[1] + ) + ( + ;3[o3,p3,o4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[k13,l13,l14,m14,n14]TE[1] + ;3[m6,l7,m7,n7,m8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[i11,h12,i12,j12,i13]TE[1] + ;2[m10,l11,m11,n11,m12]TE[1] + ;3[n9,n10,o10,p10,o11]TE[1] + ;4[f10,f11,g11,e12,f12]TE[1] + ) + ) + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o3,p3,o4,n5,o5]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[k13,l13,m13,m14,n14]TE[1] + ;3[m6,l7,m7,n7,m8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[j11,i12,j12,h13,i13]TE[1] + ;2[n10,m11,n11,o11,n12]TE[1] + ;3[i8,j8,k8,j9,j10]TE[1] + ;4[g6,i6,g7,h7,i7]TE[1] + ) + ) + ( + ;3[t1,t2,s3,t3,s4]TE[1] + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f15,e16,f16,d17,e17]TE[1] + ( + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ( + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,n9,o9,m10,n10]TE[1] + ;4[g7,g8,h8,i8,h9]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[i12,j12,k12,l12,i13] + ;3[p10,p11,q11,p12,p13]TE[1] + ;4[l8,j9,k9,l9,m9]TE[1] + ) + ( + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ) + ) + ( + ;2[o15,p15,p16,q16,q17]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[m13,l14,m14,n14,m15]TE[1] + ;3[o8,n9,o9,m10,n10]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[k11,l11,m11,n11,l12]TE[1] + ;3[p10,o11,p11,q11,p12]TE[1] + ;4[i9,j9,k9,l9,m9]TE[1] + ) + ) + ( + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[f15,e16,f16,d17,e17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[h6,h7,i7,i8,j8]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[p8,q8,p9,p10,p11]TE[1] + ;3[l11,l12,k13,l13,l14]TE[1] + ;4[h9,h10,f11,g11,h11]TE[1] + ) + ) + ) + ( + ;2[r18,r19,r20,s20,t20]TE[1] + ;3[s1,t1,s2,r3,s3]TE[1] + ;4[a1,b1,c1,c2,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,q16,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,d5,e5,f5,f6]TE[1] + ;1[j13,i14,j14,h15,i15]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ) + ( + ;2[s17,t17,t18,t19,t20] + ;3[q1,r1,s1,t1,q2]TE[1] + ;4[a1,a2,a3,a4,b4] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[p14,q14,q15,r15,r16] + ;3[o3,p3,n4,o4,n5]TE[1] + ;4[c5,d5,d6,d7,e7] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[m11,m12,n12,o12,o13] + ;3[m6,k7,l7,m7,k8]TE[1] + ;4[f8,g8,g9,h9,h10] + ;1[k9,k10,k11,k12,k13]TE[1] + ;2[l8,m8,n8,l9,l10] + ;3[j9,j10,j11,j12,j13]TE[1] + ) + ( + ;2[s17,s18,t18,t19,t20]TE[1] + ;3[s1,t1,s2,r3,s3]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[p14,p15,q15,q16,r16]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[m11,m12,n12,o12,o13]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,g8,h8,h9,h10]TE[1] + ;1[k9,k10,k11,k12,k13]TE[1] + ;2[n6,n7,n8,n9,n10]TE[1] + ;3[j9,j10,j11,j12,j13]TE[1] + ;4[i11,i12,i13,h14,i14]TE[1] + ) +) +( + ;1[a20,b20,c20,d20,e20]TE[1] + ( + ;2[s17,t17,t18,t19,t20]TE[1] + ;3[s1,t1,s2,r3,s3]TE[1] + ;4[a1,a2,a3,b3,c3]TE[1] + ;1[h17,g18,h18,f19,g19]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[i13,i14,h15,i15,i16]TE[1] + ;2[m11,m12,n12,n13,o13]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,g8,h8,h9,h10]TE[1] + ;1[g10,g11,h11,i11,h12]TE[1] + ;2[j10,k10,l10,k11,k12]TE[1] + ;3[h5,g6,h6,i6,j6]TE[1] + ;4[f9,e10,f10,f11,f12]TE[1] + ) + ( + ;2[r18,s18,s19,s20,t20]TE[1] + ;3[s1,t1,s2,r3,s3]TE[1] + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[h17,g18,h18,f19,g19]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[j13,j14,i15,j15,i16]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k9,k10,k11,l11,k12]TE[1] + ;2[m8,m9,n9,o9,n10]TE[1] + ;3[i9,j9,j10,j11,j12]TE[1] + ;4[j5,j6,j7,i8,j8]TE[1] + ) + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[h17,g18,h18,f19,g19]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[i13,i14,h15,i15,i16]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ( + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,j10,k10,j11,j12]TE[1] + ;2[l8,m8,l9,m9,l10]TE[1] + ;3[i9,j9,i10,i11,i12]TE[1] + ;4[e9,e10,f10,f11,f12]TE[1] + ) + ( + ;4[g7,g8,h8,h9,h10]TE[1] + ;1[k9,j10,k10,j11,j12]TE[1] + ;2[l8,l9,m9,l10]TE[1] + ;3[i9,j9,i10,i11,i12]TE[1] + ;4[g11,f12,g12,h12,h13]TE[1] + ) + ) + ) +) +( + ;1[a16,a17,a18,a19,a20]TE[1] + ;2[s17,t17,t18,t19,t20]TE[1] + ( + ;3[t1,t2,t3,t4,t5]TE[1] + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[c13,d13,b14,c14,b15]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[s6,r7,s7,q8,r8]TE[1] + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[f11,g11,h11,e12,f12]TE[1] + ;2[m11,n11,n12,o12,o13]TE[1] + ;3[o9,p9,m10,n10,o10]TE[1] + ;4[h6,h7,i7,i8,j8]TE[1] + ;1[k9,i10,j10,k10,l10]TE[1] + ;2[i11,j11,k11,k12,l12]TE[1] + ;3[p11,p12,p13,q13,r13]TE[1] + ;4[k7,l7,l8,m8,n8]TE[1] + ) + ( + ;3[p1,q1,r1,s1,t1]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[c13,d13,b14,c14,b15]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[n2,o2,n3,m4,n4]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[f11,g11,h11,e12,f12]TE[1] + ;2[m11,m12,n12,n13,o13]TE[1] + ;3[k5,l5,j6,k6,k7]TE[1] + ;4[g7,g8,f9,g9,h9]TE[1] + ;1[k11,i12,j12,k12,k13]TE[1] + ;2[i9,j9,k9,k10,l10]TE[1] + ;3[l8,l9,m9,m10,n10]TE[1] + ;4[e10,c11,d11,e11,d12]TE[1] + ) +) +( + ;1[a18,b18,c18,a19,a20]TE[1] + ( + ;2[s17,t17,t18,t19,t20]TE[1] + ( + ;3[q1,r1,s1,t1,q2]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[p3,o4,p4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j14,h15,i15,j15,j16]TE[1] + ;2[n12,m13,n13,o13,n14]TE[1] + ;3[k6,l6,m6,k7,k8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,k10,k11,k12,k13]TE[1] + ;2[l8,l9,l10,m10,m11]TE[1] + ;3[j9,j10,j11,j12,j13]TE[1] + ;4[h10,h11,h12,h13,h14]TE[1] + ) + ( + ;3[s1,t1,s2,r3,s3]TE[1] + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[e15,f15,e16,d17,e17]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[i13,g14,h14,i14,h15]TE[1] + ;2[m11,m12,n12,n13,o13]TE[1] + ;3[l6,m6,l7,l8,l9]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[l10,k11,l11,j12,k12]TE[1] + ;2[m8,m9,n9,o9,n10]TE[1] + ;3[j10,k10,i11,j11,i12]TE[1] + ;4[f11,f12,e13,f13,f14]TE[1] + ) + ) + ( + ;2[r18,s18,s19,s20,t20]TE[1] + ( + ;3[q1,r1,s1,t1,q2]TE[1] + ;4[a1,a2,a3,b3,c3]TE[1] + ;1[e15,f15,e16,d17,e17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[p3,n4,o4,p4,n5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j13,g14,h14,i14,j14]TE[1] + ;2[n12,m13,n13,o13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ;2[l8,l9,l10,m10,m11]TE[1] + ;3[j9,i10,j10,j11,j12]TE[1] + ;4[j6,k6,i7,j7,j8]TE[1] + ) + ( + ;3[t1,t2,r3,s3,t3]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[e15,f15,e16,d17,e17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[p4,q4,p5,o6,p6]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[h12,i12,g13,h13,g14]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[n7,n8,n9,m10,n10]TE[1] + ;4[g7,g8,h8,i8,h9]TE[1] + ;1[j10,k10,l10,j11,k11]TE[1] + ) + ) +) +( + ;1[c18,c19,a20,b20,c20]TE[1] + ;2[s17,t17,t18,t19,t20]TE[1] + ;3[t1,t2,t3,s4,t4]TE[1] + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[f15,d16,e16,f16,d17]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[i13,g14,h14,i14,i15]TE[1] + ;2[o11,p11,n12,o12,o13]TE[1] + ;3[k8,l8,m8,n8,o8]TE[1] + ;4[h6,i6,i7,j7,j8]TE[1] + ;1[k11,j12,k12,l12,k13]TE[1] + ;2[p8,p9,q9,q10,r10]TE[1] + ;3[j9,i10,j10,i11,i12]TE[1] + ;4[h8,g9,h9,h10,h11]TE[1] +) +( + ;1[c18,a19,b19,c19,a20]TE[1] + ;2[r18,s18,s19,s20,t20]TE[1] + ;3[t1,r2,s2,t2,r3]TE[1] + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[e3,f3,f4,g4,g5]TE[1] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[k13,l13,m13,m14,n14]TE[1] + ;3[m6,l7,m7,n7,m8]TE[1] + ;4[h6,g7,h7,i7,h8]TE[1] + ;1[i11,h12,i12,j12,i13]TE[1] + ;2[k15,l15,j16,k16,k17]TE[1] + ;3[n9,n10,o10,p10,n11]TE[1] + ;4[i9,i10,j10,k10,k11]TE[1] +) +) diff --git a/src/books/book_classic_3.blksgf b/src/books/book_classic_3.blksgf new file mode 100644 index 0000000..273d56e --- /dev/null +++ b/src/books/book_classic_3.blksgf @@ -0,0 +1,14 @@ +( +;GM[Blokus Three-Player] +( + ;1[a17,b17,a18,a19,a20]TE[1] + ;2[s17,t17,t18,t19,t20]TE[1] + ;3[t1,t2,t3,s4,t4]TE[1] +) +( + ;1[b17,a18,b18,a19,a20]TE[1] +) +( + ;1[a20,b20,c20,c19,c18]TE[1] +) +) diff --git a/src/books/book_duo.blksgf b/src/books/book_duo.blksgf new file mode 100644 index 0000000..6559619 --- /dev/null +++ b/src/books/book_duo.blksgf @@ -0,0 +1,212 @@ +( +;GM[Blokus Duo] +( + ;B[f9,e10,f10,g10,f11]TE[1] + ( + ;W[i4,h5,i5,j5,i6]TE[1] + ( + ;B[h7,g8,h8,h9,i9]TE[1] + ( + ;W[f5,e6,f6,g6,e7]TE[1] + ) + ( + ;W[f5,f6,g6,e7,f7]TE[1] + ) + ) + ( + ;B[g7,g8,h8,i8,h9]TE[1] + ) + ( + ;B[e7,c8,d8,e8,d9]TE[1] + ( + ;W[h7,h8,i8,h9,h10]TE[1] + ) + ( + ;W[e5,d6,e6,f6,g6]TE[1] + ) + ( + ;W[h7,h8,h9,i9,h10]TE[1] + ) + ) + ( + ;B[e6,e7,d8,e8,d9]TE[1] + ) + ( + ;B[g6,g7,h7,i7,g8]TE[1] + ( + ;W[k6,j7,k7,j8,j9]TE[1] + ) + ( + ;W[f4,g4,e5,f5,e6]TE[1] + ) + ) + ( + ;B[h6,h7,i7,g8,h8]TE[1] + ) + ( + ;B[g6,g7,g8,h8,h9] + ;W[j7,j8,j9,k9,j10]TE[1] + ;B[h3,f4,g4,h4,f5]TE[1] + ;W[h10,h11,i11,g12,h12]TE[1] + ) + ( + ;B[g6,g7,g8,h8,i8] + ;W[f4,g4,e5,f5,e6]TE[1] + ) + ( + ;B[i7,g8,h8,i8,h9]BM[1] + ;W[g6,f7,g7,h7,f8]TE[1] + ) + ) + ( + ;W[j5,i6,j6,k6,j7]TE[1] + ) +) +( + ;B[e9,d10,e10,f10,e11]TE[1] + ;W[j4,i5,j5,k5,j6]TE[1] + ( + ;B[h6,g7,h7,f8,g8]TE[1] + ) + ( + ;B[h7,f8,g8,h8,g9]TE[1] + ) +) +( + ;B[f8,e9,f9,g9,e10]TE[1] + ;W[i4,h5,i5,j5,i6]TE[1] + ( + ;B[h8,i8,j8,k8,j9]TE[1] + ( + ;W[k6,k7,l7,l8,l9]TE[1] + ) + ( + ;W[f6,g6,f7,g7,g8]TE[1] + ) + ) + ( + ;B[g4,h4,g5,g6,g7]TE[1] + ) + ( + ;B[g5,g6,g7,h7,i7]TE[1] + ( + ;W[k6,j7,k7,k8,l8]TE[1] + ) + ( + ;W[k6,j7,k7,l7,j8]TE[1] + ) + ) + ( + ;B[g6,g7,h7,h8,i8] + ;W[j7,j8,i9,j9,i10]TE[1] + ) + ( + ;B[g4,g5,f6,g6,g7] + ;W[f3,g3,h3,e4,f4]TE[1] + ;B[i7,j7,k7,h8,i8] + ;W[k6,l6,l7,k8,l8]TE[1] + ) +) +( + ;B[e8,e9,f9,d10,e10]TE[1] + ;W[i4,h5,i5,j5,i6]TE[1] + ;B[g6,f7,g7,h7,g8]TE[1] + ;W[j7,j8,j9,k9,j10]TE[1] +) +( + ;B[e8,f8,d9,e9,e10]TE[1] + ( + ;W[j5,j6,k6,i7,j7]TE[1] + ( + ;B[g6,g7,h7,h8,i8]TE[1] + ( + ;W[i3,h4,i4,g5,h5]TE[1] + ) + ( + ;W[h4,i4,f5,g5,h5]TE[1] + ) + ) + ( + ;B[h5,h6,g7,h7,h8]TE[1] + ;W[f3,f4,g4,h4,i4]TE[1] + ) + ) + ( + ;W[j5,i6,j6,k6,j7]TE[1] + ;B[h5,i5,h6,g7,h7] + ( + ;W[g8,h8,i8,h9,i9]TE[1] + ) + ( + ;W[i8,i9,h10,i10,h11]TE[1] + ) + ) + ( + ;W[i4,h5,i5,j5,i6] + ;B[g4,g5,g6,g7,h7]TE[1] + ;W[g2,f3,g3,h3,f4] + ;B[k6,j7,k7,i8,j8]TE[1] + ) +) +( + ;B[f8,d9,e9,f9,e10]TE[1] + ;W[j5,i6,j6,k6,i7]TE[1] + ;B[h5,h6,g7,h7,h8]TE[1] + ( + ;W[g3,f4,g4,h4,i4]TE[1] + ) + ( + ;W[g4,h4,i4,f5,g5] + ;B[j8,k8,l8,i9,j9]TE[1] + ) +) +( + ;B[e8,d9,e9,e10,f10]TE[1] + ( + ;W[j5,i6,j6,k6,j7]TE[1] + ;B[f4,e5,f5,f6,f7]TE[1] + ( + ;W[f3,g3,g4,g5,h5]TE[1] + ) + ( + ;W[h7,h8,h9,i9,h10]TE[1] + ) + ( + ;W[g7,h7,f8,g8,f9]TE[1] + ) + ) + ( + ;W[i4,h5,i5,j5,i6]TE[1] + ;B[g4,g5,f6,g6,f7]TE[1] + ( + ;W[g2,f3,g3,h3,f4]TE[1] + ) + ( + ;W[g7,h7,f8,g8,f9]TE[1] + ) + ) +) +( + ;B[f7,f8,e9,f9,e10]TE[1] + ;W[i4,i5,j5,h6,i6]TE[1] + ;B[h4,f5,g5,h5,g6]TE[1] +) +( + ;B[e7,e8,d9,e9,e10] + ;W[j5,i6,j6,h7,i7]TE[1] + ;B[h4,f5,g5,h5,f6] + ;W[f7,f8,g8,f9,f10]TE[1] +) +( + ;B[g9,e10,f10,g10,g11] + ;W[i4,h5,i5,j5,i6]TE[1] + ;B[j6,h7,i7,j7,h8] + ;W[f6,g6,f7,g7,g8]TE[1] +) +( + ;B[e10,f10,g10,h10,g11] + ;W[i4,h5,i5,j5,i6]TE[1] + ;B[j6,i7,j7,i8,i9] + ;W[e5,e6,f6,g6,g7]TE[1] +) +) diff --git a/src/books/book_junior.blksgf b/src/books/book_junior.blksgf new file mode 100644 index 0000000..9d86c8e --- /dev/null +++ b/src/books/book_junior.blksgf @@ -0,0 +1,9 @@ +( +;GM[Blokus Junior] +( + ;B[f9,e10,f10,e11,f11]TE[1] +) +( + ;B[g9,d10,e10,f10,g10]TE[1] +) +) diff --git a/src/books/book_nexos.blksgf b/src/books/book_nexos.blksgf new file mode 100644 index 0000000..7793fc6 --- /dev/null +++ b/src/books/book_nexos.blksgf @@ -0,0 +1,15 @@ +( +;GM[Nexos] +( + ;1[g16,g18,f19,e20]TE[1] +) +( + ;1[h17,g18,g20,f21]TE[1] +) +( + ;1[h17,g18,f19,e20]TE[1] +) +( + ;1[g16,g18,g20,f21]TE[1] +) +) diff --git a/src/books/book_nexos_2.blksgf b/src/books/book_nexos_2.blksgf new file mode 100644 index 0000000..e535a52 --- /dev/null +++ b/src/books/book_nexos_2.blksgf @@ -0,0 +1,15 @@ +( +;GM[Nexos Two-Player] +( + ;1[g16,g18,f19,e20]TE[1] +) +( + ;1[h17,g18,g20,f21]TE[1] +) +( + ;1[h17,g18,f19,e20]TE[1] +) +( + ;1[g16,g18,g20,f21]TE[1] +) +) diff --git a/src/books/book_trigon.blksgf b/src/books/book_trigon.blksgf new file mode 100644 index 0000000..da0292e --- /dev/null +++ b/src/books/book_trigon.blksgf @@ -0,0 +1,9 @@ +( +;GM[Blokus Trigon] +( + ;1[r12,r13,s13,r14,s14,r15]TE[1] +) +( + ;1[t12,s13,t13,r14,s14,r15]TE[1] +) +) diff --git a/src/books/book_trigon_2.blksgf b/src/books/book_trigon_2.blksgf new file mode 100644 index 0000000..99b9f7e --- /dev/null +++ b/src/books/book_trigon_2.blksgf @@ -0,0 +1,25 @@ +( +;GM[Blokus Trigon Two-Player] +( + ;1[r12,r13,s13,r14,s14,r15]TE[1] + ( + ;2[r4,q5,r5,q6,r6,r7]TE[1] + ;3[j7,k7,l7,m7,m8,n8]TE[1] + ;4[v11,w11,w12,x12,y12,z12]TE[1] + ;1[n9,o9,o10,p10,p11,q11]BM[1] + ;2[j6,k6,l6,m6,n6,o6]TE[1] + ) + ( + ;2[r4,r5,s5,r6,s6,r7] + ) + ( + ;2[r4,q5,r5,p6,q6,p7]TE[1] + ) +) +( + ;1[t12,s13,t13,r14,s14,r15]TE[1] + ;2[r4,q5,r5,p6,q6,p7]TE[1] + ;3[j7,k7,l7,m7,n7,o7]TE[1] + ;4[u12,v12,w12,x12,y12,z12]TE[1] +) +) diff --git a/src/books/book_trigon_3.blksgf b/src/books/book_trigon_3.blksgf new file mode 100644 index 0000000..7de2ba6 --- /dev/null +++ b/src/books/book_trigon_3.blksgf @@ -0,0 +1,9 @@ +( +;GM[Blokus Trigon Three-Player] +( + ;1[p11,o12,p12,o13,p13,p14]TE[1] +) +( + ;1[r11,q12,r12,p13,q13,p14]TE[1] +) +) diff --git a/src/books/pentobi_books.qrc b/src/books/pentobi_books.qrc new file mode 100644 index 0000000..3adda93 --- /dev/null +++ b/src/books/pentobi_books.qrc @@ -0,0 +1,17 @@ + + + book_callisto.blksgf + book_callisto_2.blksgf + book_callisto_3.blksgf + book_classic.blksgf + book_classic_2.blksgf + book_classic_3.blksgf + book_duo.blksgf + book_junior.blksgf + book_nexos.blksgf + book_nexos_2.blksgf + book_trigon.blksgf + book_trigon_2.blksgf + book_trigon_3.blksgf + + diff --git a/src/convert/CMakeLists.txt b/src/convert/CMakeLists.txt new file mode 100644 index 0000000..20bddab --- /dev/null +++ b/src/convert/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(convert Main.cpp) + +target_link_libraries(convert Qt5::Widgets) diff --git a/src/convert/Main.cpp b/src/convert/Main.cpp new file mode 100644 index 0000000..6ba8a50 --- /dev/null +++ b/src/convert/Main.cpp @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------------- +/** @file convert/Main.cpp + Utility program for converting icons between image formats. + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include +#include +#include +#include +#include + +//----------------------------------------------------------------------------- + +int main(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + try + { + QCommandLineParser parser; + QCommandLineOption optionHdpi("hdpi"); + parser.addOption(optionHdpi); + parser.process(app); + auto args = parser.positionalArguments(); + if (args.size() != 2) + throw QString("Need two arguments"); + auto in = args.at(0); + auto out = args.at(1); + QImageReader reader(in); + QImage image = reader.read(); + if (image.isNull()) + throw QString("%1: %2").arg(in, reader.errorString()); + if (parser.isSet(optionHdpi)) + { + QImageReader reader(in); + reader.setScaledSize(2 * image.size()); + image = reader.read(); + if (image.isNull()) + throw QString("%1: %2").arg(in, reader.errorString()); + } + QImageWriter writer(out); + if (! writer.write(image)) + throw QString("%1: %2").arg(out, writer.errorString()); + } + catch (const QString& msg) + { + std::cerr << msg.toLocal8Bit().constData() << '\n'; + return 1; + } + return 0; +} + +//----------------------------------------------------------------------------- diff --git a/src/doc_libboardgame.cpp b/src/doc_libboardgame.cpp new file mode 100644 index 0000000..0081b4d --- /dev/null +++ b/src/doc_libboardgame.cpp @@ -0,0 +1,76 @@ +/** + +@page libboardgame_doc_tags Tags used in documentation +This page defines attributes of documentation elements that are in +widespread use. For brevity, the documentation block contains only +a reference to the section of this page. + +@section libboardgame_avoid_stack_allocation Class size is large +The size of this class is large because it contains large members that are not +allocated on the heap to avoid dereferencing pointers for speed reasons. It +should be avoided to create instances of this class on the stack. + +@section libboardgame_doc_obj_ref_opt Object reference optimization +This class uses a reference to a certain object several times but does not +store the reference at construction time for memory and/or speed optimization. +The reference is passed as an argument to the functions that need it. The +class instance assumes (and might check with assertions) that the reference +always refers to the same object . + +@section libboardgame_doc_storesref Stores a reference +Used for parameters to indicate that the class will store a reference to the +parameter. The lifetime of the parameter must exceed the lifetime of the +constructed class. + +@section libboardgame_doc_threadsafe_after_construction Thread-safe after +construction +Used for classes that, that are thread-safe (w.r.t. different instances) after +construction. The constructor (and potentially also the destructor) is not +thread-safe, for example because it modifies non-const static class members. + + +@page libboardgame_doc_glossary Glossary +This page explains and defines terms used in the documentation. + +@section libboardgame_doc_gogui GoGui +Graphical interface for Go engines using GTP. Defines several GTP extension +commands. http://gogui.sf.net + +@section libboardgame_doc_gnugo GNU Go +GNU Go program http://www.gnu.org/s/gnugo/ + +@section libboardgame_doc_gtp GTP +Go Text Protocol http://www.lysator.liu.se/~gunnar/gtp/ + +@section libboardgame_doc_uct UCT +Upper Confidence bounds applied to Tree: a Monte-Carlo tree search algorithm +that applies bandit ideas to the move selection at tree nodes. +See @ref libboardgame_doc_kocsis_szepesvari_2006 + +@section libboardgame_doc_rave RAVE +Rapid Action Value Estimation: Keeps track of the value of a move averaged +over all simulations in the subtree of a node in which the move was played +by a player in a position following the node (inclusive). +See @ref libboardgame_doc_gelly_silver_2007 + +@section libboardgame_doc_sgf SGF +Smart Game Format http://www.red-bean.com/sgf/ + +@page libboardgame_doc_bibliography Bibliography +List of publications. + +@section libboardgame_doc_enz_2009 A Lock-free Multithreaded Monte-Carlo Tree Search Algorithm. +M. Enzenberger, M. Mueller. Advances in Computer Games 2009. +(PDF) + +@section libboardgame_doc_gelly_silver_2007 Combining Online and Offline Knowledge in UCT. +S. Gelly, D. Silver. Proceedings of the 24th international conference on Machine learning, pp. 273-280, 2007. +(PDF) + +@section libboardgame_doc_kocsis_szepesvari_2006 Bandit Based Monte-Carlo Planning +L. Kocsis, Cs. Szepesvári. Proceedings of the 17th European Conference on +Machine Learning, Springer-Verlag, Berlin, LNCS/LNAI 4212, September 18-22, +pp. 282-293, 2006 +(PDF) + +*/ diff --git a/src/doc_mainpage.cpp b/src/doc_mainpage.cpp new file mode 100644 index 0000000..978b7a1 --- /dev/null +++ b/src/doc_mainpage.cpp @@ -0,0 +1,72 @@ +/** @mainpage notitle + + @section mainpage_libboardgame LibBoardGame Modules + + The LibBoardGame modules contain code that is not specific to the board + game Blokus and could be reused for other projects: + + - libboardgame_gtp - + Implementation of the Go Text Protocol GTP (@ref libboardgame_doc_gtp) + - libboardgame_sys - + Platform-dependent functionality + - libboardgame_util - + General utilities not specific to board games + - libboardgame_sgf - + Implementation of the Smart Game Format (@ref libboardgame_doc_sgf) + - libboardgame_base - + Utility classes and functions specific to board games + - libboardgame_mcts - + Monte-Carlo tree search + - libboardgame_test - + Functionality for unit tests similar to Boost::Test + + @section mainpage_pentobi Pentobi Modules + + The Pentobi modules are specific to the board game Blokus: + + - libpentobi_base - + General Blokus-specific functionality + - libpentobi_mcts - + Blokus player based on Monte-Carlo tree search + - pentobi_gtp - + GTP interface to the player in libpentobi_mcts + - twogtp - + Tool for playing games between two GTP engines + (currently only supported on Linux/GCC) + + @section mainpage_gui Pentobi QWidgets GUI Modules + + The Pentobi QWidgets GUI modules implement a user interface based on + Qt/QWidgets and targeted at desktops. + They have a dependency on the following + Qt libraries: QtCore4, QtGui4. + They are currently used for the desktop versions of Pentobi. + They may become obsolete in the future, once the QML GUI Modules + (@ref mainpage_gui_qml) provide the same functionality. + + - convert - + Small helper program to convert SVG icons to bitmaps at build time + - libpentobi_gui - + GUI functionality that could be reused for other projects + - libpentobi_thumbnail - + Common functionality for file preview thumbnailers + - pentobi - + Main program that provides a GUI for the player in libpentobi_mcts + - pentobi_thumbnailer - + Generates file preview thumbnails for the + Gnome desktop + - pentobi_kde_thumbnailer - + Plugin for file preview thumbnails for the + KDE desktop + + @section mainpage_gui_qml Pentobi QML GUI Modules + + The Pentobi QML GUI modules implement a user interface based on + Qt Quick / QML. They currently support only a subset of the features + of the QWidgets-based GUI (@ref mainpage_gui) but provide fluid + animations and are usable on touch-screens. They are currently + used for the Android version of Pentobi. + + - pentobi_qml - + Main program that provides a GUI for the player in libpentobi_mcts +*/ diff --git a/src/libboardgame_base/CMakeLists.txt b/src/libboardgame_base/CMakeLists.txt new file mode 100644 index 0000000..d51513d --- /dev/null +++ b/src/libboardgame_base/CMakeLists.txt @@ -0,0 +1,22 @@ +add_library(boardgame_base STATIC + CoordPoint.h + CoordPoint.cpp + Engine.h + Engine.cpp + Geometry.h + GeometryUtil.h + Grid.h + Marker.h + Point.h + PointTransform.h + Rating.h + Rating.cpp + RectGeometry.h + RectTransform.h + RectTransform.cpp + StringRep.h + StringRep.cpp + Transform.h + Transform.cpp +) + diff --git a/src/libboardgame_base/CoordPoint.cpp b/src/libboardgame_base/CoordPoint.cpp new file mode 100644 index 0000000..da120fe --- /dev/null +++ b/src/libboardgame_base/CoordPoint.cpp @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/CoordPoint.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "CoordPoint.h" + +#include + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const CoordPoint& p) +{ + if (! p.is_null()) + out << '(' << p.x << ',' << p.y << ')'; + else + out << "NULL"; + return out; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/CoordPoint.h b/src/libboardgame_base/CoordPoint.h new file mode 100644 index 0000000..6761272 --- /dev/null +++ b/src/libboardgame_base/CoordPoint.h @@ -0,0 +1,128 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/CoordPoint.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_COORD_POINT_H +#define LIBBOARDGAME_BASE_COORD_POINT_H + +#include +#include + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** %Point stored as x,y coordinates. */ +struct CoordPoint +{ + int x; + + int y; + + static bool is_onboard(int x, int y, unsigned width, unsigned height); + + static CoordPoint null(); + + CoordPoint() = default; + + CoordPoint(int x, int y); + + bool operator==(const CoordPoint& p) const; + + bool operator!=(const CoordPoint& p) const; + + bool operator<(const CoordPoint& p) const; + + CoordPoint operator+(const CoordPoint& p) const; + + CoordPoint operator-(const CoordPoint& p) const; + + CoordPoint& operator+=(const CoordPoint& p); + + CoordPoint& operator-=(const CoordPoint& p); + + bool is_null() const; + + bool is_onboard(unsigned width, unsigned height) const; +}; + +inline CoordPoint::CoordPoint(int x, int y) +{ + this->x = x; + this->y = y; +} + +inline bool CoordPoint::operator==(const CoordPoint& p) const +{ + return x == p.x && y == p.y; +} + +inline bool CoordPoint::operator<(const CoordPoint& p) const +{ + if (y != p.y) + return y < p.y; + return x < p.x; +} + +inline bool CoordPoint::operator!=(const CoordPoint& p) const +{ + return ! operator==(p); +} + +inline CoordPoint CoordPoint::operator+(const CoordPoint& p) const +{ + return CoordPoint(x + p.x, y + p.y); +} + +inline CoordPoint& CoordPoint::operator+=(const CoordPoint& p) +{ + *this = *this + p; + return *this; +} + +inline CoordPoint CoordPoint::operator-(const CoordPoint& p) const +{ + return CoordPoint(x - p.x, y - p.y); +} + +inline CoordPoint& CoordPoint::operator-=(const CoordPoint& p) +{ + *this = *this - p; + return *this; +} + +inline CoordPoint CoordPoint::null() +{ + return CoordPoint(numeric_limits::max(), numeric_limits::max()); +} + +inline bool CoordPoint::is_onboard(int x, int y, unsigned width, + unsigned height) +{ + return x >= 0 && x < static_cast(width) + && y >= 0 && y < static_cast(height); +} + +inline bool CoordPoint::is_onboard(unsigned width, unsigned height) const +{ + return is_onboard(x, y, width, height); +} + +inline bool CoordPoint::is_null() const +{ + return x == numeric_limits::max(); +} + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const CoordPoint& p); + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_COORD_POINT_H diff --git a/src/libboardgame_base/Engine.cpp b/src/libboardgame_base/Engine.cpp new file mode 100644 index 0000000..195f849 --- /dev/null +++ b/src/libboardgame_base/Engine.cpp @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Engine.h" + +#include "libboardgame_sys/CpuTime.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/RandomGenerator.h" + +namespace libboardgame_base { + +using namespace std; +using libboardgame_gtp::Failure; +using libboardgame_util::flush_log; +using libboardgame_util::RandomGenerator; + +//----------------------------------------------------------------------------- + +Engine::Engine() +{ + add("cputime", &Engine::cmd_cputime); + add("set_random_seed", &Engine::cmd_set_random_seed); +} + +Engine::~Engine() = default; + +void Engine::cmd_cputime(Response& response) +{ + double time = libboardgame_sys::cpu_time(); + if (time == -1) + throw Failure("cannot determine cpu time"); + response << time; +} + +/** Set global random seed. + Compatible with @ref libboardgame_doc_gnugo
+ Arguments: random seed */ +void Engine::cmd_set_random_seed(const Arguments& args) +{ + RandomGenerator::set_global_seed(args.parse()); +} + +void Engine::on_handle_cmd_begin() +{ + flush_log(); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/Engine.h b/src/libboardgame_base/Engine.h new file mode 100644 index 0000000..4498260 --- /dev/null +++ b/src/libboardgame_base/Engine.h @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Engine.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_ENGINE_H +#define LIBBOARDGAME_BASE_ENGINE_H + +#include "libboardgame_gtp/Engine.h" + +namespace libboardgame_base { + +using libboardgame_gtp::Arguments; +using libboardgame_gtp::Response; + +//----------------------------------------------------------------------------- + +class Engine + : public libboardgame_gtp::Engine +{ +public: + void cmd_cputime(Response&); + void cmd_set_random_seed(const Arguments&); + + Engine(); + + ~Engine(); + +protected: + void on_handle_cmd_begin() override; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_ENGINE_H diff --git a/src/libboardgame_base/Geometry.h b/src/libboardgame_base/Geometry.h new file mode 100644 index 0000000..27bdc48 --- /dev/null +++ b/src/libboardgame_base/Geometry.h @@ -0,0 +1,343 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Geometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_GEOMETRY_H +#define LIBBOARDGAME_BASE_GEOMETRY_H + +#include +#include +#include "CoordPoint.h" +#include "StringRep.h" +#include "libboardgame_util/ArrayList.h" + +namespace libboardgame_base { + +using namespace std; +using libboardgame_util::ArrayList; + +//----------------------------------------------------------------------------- + +/** %Geometry data of a board with a given size. + This class is a base class that uses virtual functions in its constructor + that allow to restrict the shape of the board to a subset of the rectangle + and/or to define different definitions of adjacent and diagonal neighbors + of a point for geometries that are not a regular rectangular grid. + @tparam P An instantiation of libboardgame_base::Point (or compatible + class) + @tparam S A class with functions to convert points from and to strings + depending on the string representation of points in the game. */ +template +class Geometry +{ +public: + typedef P Point; + + typedef typename Point::IntType IntType; + + /** On-board adjacent neighbors of a point. */ + typedef ArrayList AdjList; + + /** On-board diagonal neighbors of a point + Currently supports up to nine diagonal points as used on boards + for Blokus Trigon. */ + typedef ArrayList DiagList; + + /** Adjacent neighbors of a coordinate. */ + typedef ArrayList AdjCoordList; + + /** Diagonal neighbors of a coordinate. */ + typedef ArrayList DiagCoordList; + + class Iterator + { + public: + explicit Iterator(IntType i) { m_i = i; } + + bool operator==(Iterator it) const { return m_i == it.m_i; } + + bool operator!=(Iterator it) const { return m_i != it.m_i; } + + void operator++() { ++m_i; } + + Point operator*() const { return Point(m_i); } + + private: + IntType m_i; + }; + + virtual ~Geometry(); + + virtual AdjCoordList get_adj_coord(int x, int y) const = 0; + + virtual DiagCoordList get_diag_coord(int x, int y) const = 0; + + /** Return the point type if the board has different types of points. + For example, in the geometry used in Blokus Trigon, there are two + point types (0=upward triangle, 1=downward triangle); in a regular + rectangle, there is only one point type. By convention, 0 is the + type of the point at (0,0). + @param x The x coordinate (may be negative and/or outside the board). + @param y The y coordinate (may be negative and/or outside the board). */ + virtual unsigned get_point_type(int x, int y) const = 0; + + /** Get repeat interval for point types along the x axis. + If the board has different point types, the layout of the point types + repeats in this x interval. If the board has only one point type, + the function should return 1. */ + virtual unsigned get_period_x() const = 0; + + /** Get repeat interval for point types along the y axis. + @see get_period_x(). */ + virtual unsigned get_period_y() const = 0; + + Iterator begin() const { return Iterator(0); } + + Iterator end() const { return Iterator(get_range()); } + + unsigned get_point_type(CoordPoint p) const; + + unsigned get_point_type(Point p) const; + + bool is_onboard(unsigned x, unsigned y) const; + + bool is_onboard(CoordPoint p) const; + + /** Return the point at a given coordinate. + @pre x < get_width() + @pre y < get_height() + @return The point or Point::null() if this coordinates are + off-board. */ + Point get_point(unsigned x, unsigned y) const; + + unsigned get_width() const; + + unsigned get_height() const; + + /** Get range used for onboard points. */ + IntType get_range() const; + + unsigned get_x(Point p) const; + + unsigned get_y(Point p) const; + + bool from_string(const string& s, Point& p) const; + + const string& to_string(Point p) const; + + const AdjList& get_adj(Point p) const; + + const DiagList& get_diag(Point p) const; + +protected: + explicit Geometry(unique_ptr string_rep = + unique_ptr(new StdStringRep)); + + /** Initialize. + Subclasses must call this function in their constructors. */ + void init(unsigned width, unsigned height); + + /** Initialize on-board points. + This function is used in init() and allows the subclass to restrict the + on-board points to a subset of the on-board points of a rectangle to + support different board shapes. It will only be called with x and + y within the width and height of the geometry. */ + virtual bool init_is_onboard(unsigned x, unsigned y) const = 0; + +private: + AdjList m_adj[Point::range_onboard]; + + DiagList m_diag[Point::range_onboard]; + + IntType m_range; + + Point m_points[Point::max_width][Point::max_height]; + + unique_ptr m_string_rep; + + unsigned m_width; + + unsigned m_height; + + unsigned m_x[Point::range_onboard]; + + unsigned m_y[Point::range_onboard]; + + unsigned m_point_type[Point::range_onboard]; + + string m_string[Point::range]; + +#if LIBBOARDGAME_DEBUG + bool is_valid(Point p) const; +#endif +}; + + +template +Geometry

::Geometry(unique_ptr string_rep) + : m_string_rep(move(string_rep)) +{ } + +template +Geometry

::~Geometry() = default; + +template +bool Geometry

::from_string(const string& s, Point& p) const +{ + istringstream in(s); + unsigned x; + unsigned y; + if (m_string_rep->read(in, m_width, m_height, x, y) + && is_onboard(CoordPoint(x, y))) + { + p = get_point(x, y); + return true; + } + return false; +} + +template +inline auto Geometry

::get_adj(Point p) const -> const AdjList& +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_adj[p.to_int()]; +} + +template +inline auto Geometry

::get_diag(Point p) const -> const DiagList& +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_diag[p.to_int()]; +} + +template +inline unsigned Geometry

::get_height() const +{ + return m_height; +} + +template +inline auto Geometry

::get_point(unsigned x, unsigned y) const -> Point +{ + LIBBOARDGAME_ASSERT(x < m_width); + LIBBOARDGAME_ASSERT(y < m_height); + return m_points[x][y]; +} + +template +inline unsigned Geometry

::get_point_type(Point p) const +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_point_type[p.to_int()]; +} + +template +inline unsigned Geometry

::get_point_type(CoordPoint p) const +{ + return get_point_type(p.x, p.y); +} + +template +inline auto Geometry

::get_range() const -> IntType +{ + return m_range; +} + +template +inline unsigned Geometry

::get_width() const +{ + return m_width; +} + +template +inline unsigned Geometry

::get_x(Point p) const +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_x[p.to_int()]; +} + +template +inline unsigned Geometry

::get_y(Point p) const +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_y[p.to_int()]; +} + +template +void Geometry

::init(unsigned width, unsigned height) +{ + LIBBOARDGAME_ASSERT(width >= 1); + LIBBOARDGAME_ASSERT(height >= 1); + LIBBOARDGAME_ASSERT(width <= Point::max_width); + LIBBOARDGAME_ASSERT(height <= Point::max_height); + m_width = width; + m_height = height; + m_string[Point::null().to_int()] = "null"; + IntType n = 0; + ostringstream ostr; + for (unsigned y = 0; y < height; ++y) + for (unsigned x = 0; x < width; ++x) + if (init_is_onboard(x, y)) + { + m_points[x][y] = Point(n); + m_x[n] = x; + m_y[n] = y; + ostr.str(""); + m_string_rep->write(ostr, x, y, width, height); + m_string[n] = ostr.str(); + ++n; + } + else + m_points[x][y] = Point::null(); + m_range = n; + for (IntType i = 0; i < m_range; ++i) + { + Point p(i); + auto x = get_x(p); + auto y = get_y(p); + for (auto& p : get_adj_coord(x, y)) + if (is_onboard(p)) + m_adj[i].push_back(get_point(p.x, p.y)); + for (auto& p : get_diag_coord(x, y)) + if (is_onboard(p)) + m_diag[i].push_back(get_point(p.x, p.y)); + m_point_type[i] = get_point_type(x, y); + } +} + +template +inline bool Geometry

::is_onboard(unsigned x, unsigned y) const +{ + return ! get_point(x, y).is_null(); +} + +template +bool Geometry

::is_onboard(CoordPoint p) const +{ + return p.is_onboard(m_width, m_height) && is_onboard(p.x, p.y); +} + +#if LIBBOARDGAME_DEBUG + +template +inline bool Geometry

::is_valid(Point p) const +{ + return ! p.is_null() && p.to_int() < get_range(); +} + +#endif + +template +inline const string& Geometry

::to_string(Point p) const +{ + LIBBOARDGAME_ASSERT(p.to_int() < get_range()); + return m_string[p.to_int()]; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_GEOMETRY_H diff --git a/src/libboardgame_base/GeometryUtil.h b/src/libboardgame_base/GeometryUtil.h new file mode 100644 index 0000000..99ff8f2 --- /dev/null +++ b/src/libboardgame_base/GeometryUtil.h @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/GeometryUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_GEOMETRY_UTIL_H +#define LIBBOARDGAME_BASE_GEOMETRY_UTIL_H + +#include "Geometry.h" + +namespace libboardgame_base { +namespace geometry_util { + +//----------------------------------------------------------------------------- + +/** Shift a list of points as close to the (0,0) point as possible. + This will minimize the minimum x and y coordinates. The function also + returns the width and height of the bounding box and the offset that was + subtracted from the points for the shifting. + @note This transformation does not preserve point types. If the original + list was compatible with the point types on the board, the new point type of + (0,0) will be Geometry::get_point_type(offset). + @tparam T An iterator over a container containing CoordPoint element. */ +template +void normalize_offset(T begin, T end, unsigned& width, unsigned& height, + CoordPoint& offset) +{ + int min_x = numeric_limits::max(); + int min_y = numeric_limits::max(); + int max_x = numeric_limits::min(); + int max_y = numeric_limits::min(); + for (auto i = begin; i != end; ++i) + { + if (i->x < min_x) + min_x = i->x; + if (i->x > max_x) + max_x = i->x; + if (i->y < min_y) + min_y = i->y; + if (i->y > max_y) + max_y = i->y; + } + width = max_x - min_x + 1; + height = max_y - min_y + 1; + offset = CoordPoint(min_x, min_y); + for (auto i = begin; i != end; ++i) + *i -= offset; +} + +/** Get an offset to shift points that are not compatible with the point types + used in the geometry. + The offset shifts points in a minimal positive direction to match the + types, x-direction is preferred. + @param geo + @param point_type The point type of (0, 0) of the coordinate system used by + the points. */ +template +CoordPoint type_match_offset(const Geometry

& geo, unsigned point_type) +{ + for (unsigned y = 0; y < geo.get_period_y(); ++y) + for (unsigned x = 0; x < geo.get_period_x(); ++x) + if (geo.get_point_type(x, y) == point_type) + return CoordPoint(x, y); + LIBBOARDGAME_ASSERT(false); + return CoordPoint(0, 0); +} + +/** Apply type_match_offset() to a list of points. + @tparam T An iterator over a container containing CoordPoint elements. + @param geo The geometry. + @param begin The beginning of the list of points. + @param end The end of the list of points. + @param point_type The point type of (0,0) in the list of points. */ +template +void type_match_shift(const Geometry

& geo, T begin, T end, + unsigned point_type) +{ + CoordPoint offset = type_match_offset(geo, point_type); + for (auto i = begin; i != end; ++i) + *i += offset; +} + +//----------------------------------------------------------------------------- + +} // namespace geometry_util +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_GEOMETRY_UTIL_H diff --git a/src/libboardgame_base/Grid.h b/src/libboardgame_base/Grid.h new file mode 100644 index 0000000..1e1edd1 --- /dev/null +++ b/src/libboardgame_base/Grid.h @@ -0,0 +1,226 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Grid.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_GRID_H +#define LIBBOARDGAME_BASE_GRID_H + +#include +#include +#include +#include +#include +#include "Geometry.h" + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +template +string grid_to_string(const T& grid, const Geometry& geo) +{ + ostringstream buffer; + size_t max_len = 0; + for (auto p : geo) + { + buffer.str(""); + buffer << grid[p]; + max_len = max(max_len, buffer.str().length()); + } + buffer.str(""); + auto width = geo.get_width(); + auto height = geo.get_height(); + string empty(max_len, ' '); + for (unsigned y = 0; y < height; ++y) + { + for (unsigned x = 0; x < width; ++x) + { + auto p = geo.get_point(x, y); + if (! p.is_null()) + buffer << setw(int(max_len)) << grid[p]; + else + buffer << empty; + if (x < width - 1) + buffer << ' '; + } + buffer << '\n'; + } + return buffer.str(); +} + +//----------------------------------------------------------------------------- + +template class GridExt; + +/** Elements assigned to on-board points. + The elements must be default-constructible. This class is a POD if the + element type is a POD. + @tparam P An instantiation of libboardgame_base::Point (or compatible + class) + @tparam T The element type. */ +template +class Grid +{ + friend class GridExt; // for GridExt::copy_from(Grid) + +public: + typedef P Point; + + typedef libboardgame_base::Geometry

Geometry; + + T& operator[](const Point& p); + + const T& operator[](const Point& p) const; + + /** Fill all on-board points for a given geometry with a value. */ + void fill(const T& val, const Geometry& geo); + + /** Fill points with a value. */ + void fill_all(const T& val); + + string to_string(const Geometry& geo) const; + + void copy_from(const Grid& grid, const Geometry& geo); + + /** Specialized version for trivially copyable elements. + Can be used instead of copy_from if the compiler is not smart enough to + figure out that it can use memcpy. + @pre std::is_trivially_copyable::value */ + void memcpy_from(const Grid& grid, const Geometry& geo); + +private: + T m_a[Point::range_onboard]; +}; + +template +inline T& Grid::operator[](const Point& p) +{ + LIBBOARDGAME_ASSERT(! p.is_null()); + return m_a[p.to_int()]; +} + +template +inline const T& Grid::operator[](const Point& p) const +{ + LIBBOARDGAME_ASSERT(! p.is_null()); + return m_a[p.to_int()]; +} + +template +inline void Grid::copy_from(const Grid& grid, const Geometry& geo) +{ + copy(grid.m_a, grid.m_a + geo.get_range(), m_a); +} + +template +inline void Grid::fill(const T& val, const Geometry& geo) +{ + std::fill(m_a, m_a + geo.get_range(), val); +} + +template +inline void Grid::fill_all(const T& val) +{ + std::fill(m_a, m_a + Point::range_onboard, val); +} + +template +void Grid::memcpy_from(const Grid& grid, const Geometry& geo) +{ + // std::is_trivially_copyable is not available with GCC < 5 +#if ! (__GNUC__ && __GNUC__ < 5) + static_assert(is_trivially_copyable::value, ""); +#endif + memcpy(&m_a, grid.m_a, geo.get_range() * sizeof(T)); +} + +template +string Grid::to_string(const Geometry& geo) const +{ + return grid_to_string(*this, geo); +} + +//----------------------------------------------------------------------------- + +/** Like Grid, but allows Point::null() as index. */ +template +class GridExt +{ +public: + typedef P Point; + + typedef libboardgame_base::Geometry

Geometry; + + T& operator[](const Point& p); + + const T& operator[](const Point& p) const; + + /** Fill all on-board points for a given geometry with a value. */ + void fill(const T& val, const Geometry& geo); + + /** Fill points with a value. */ + void fill_all(const T& val); + + string to_string(const Geometry& geo) const; + + void copy_from(const Grid& grid, const Geometry& geo); + + void copy_from(const GridExt& grid, const Geometry& geo); + +private: + T m_a[Point::range]; +}; + +template +inline T& GridExt::operator[](const Point& p) +{ + return m_a[p.to_int()]; +} + +template +inline const T& GridExt::operator[](const Point& p) const +{ + return m_a[p.to_int()]; +} + +template +inline void GridExt::fill(const T& val, const Geometry& geo) +{ + std::fill(m_a, m_a + geo.get_range(), val); +} + +template +inline void GridExt::fill_all(const T& val) +{ + std::fill(m_a, m_a + Point::range, val); +} + +template +inline void GridExt::copy_from(const Grid& grid, + const Geometry& geo) +{ + copy(grid.m_a, grid.m_a + geo.get_range(), m_a); +} + +template +inline void GridExt::copy_from(const GridExt& grid, + const Geometry& geo) +{ + copy(grid.m_a, grid.m_a + geo.get_range(), m_a); +} + +template +string GridExt::to_string(const Geometry& geo) const +{ + return grid_to_string(*this, geo); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_GRID_H diff --git a/src/libboardgame_base/Marker.h b/src/libboardgame_base/Marker.h new file mode 100644 index 0000000..591fb0a --- /dev/null +++ b/src/libboardgame_base/Marker.h @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Marker.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_MARKER_H +#define LIBBOARDGAME_BASE_MARKER_H + +#include +#include + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** %Marker to mark points on board with fast operation to clear all marks. + This marker is typically used in recursive fills or other loops to + remember what points have already been visited. + @tparam P An instantiation of libboardgame_base::Point */ +template +class Marker +{ +public: + typedef P Point; + + Marker(); + + void clear(); + + /** Mark a point. + @return true if the point was already marked. */ + bool set(Point p); + + bool operator[](Point p) const; + + /** Set up for overflow test (for testing purposes only). + The function is equivalent to calling reset() and then clear() + nu_clear times. It allows a faster implementation of a unit test case + that tests if the overflow is handled correctly, if clear() is called + more than numeric_limits::max() times. */ + void setup_for_overflow_test(unsigned nu_clear); + +private: + unsigned m_current; + + unsigned m_a[Point::range]; + + void reset(); +}; + +template +inline Marker

::Marker() +{ + reset(); +} + +template +bool Marker

::operator[](Point p) const +{ + return m_a[p.to_int()] == m_current; +} + +template +inline void Marker

::clear() +{ + if (--m_current == 0) + reset(); +} + +template +inline void Marker

::setup_for_overflow_test(unsigned nu_clear) +{ + reset(); + m_current -= nu_clear; +} + +template +inline void Marker

::reset() +{ + m_current = numeric_limits::max() - 1; + fill(m_a, m_a + Point::range, numeric_limits::max()); +} + +template +inline bool Marker

::set(Point p) +{ + auto& a = m_a[p.to_int()]; + if (a == m_current) + return true; + a = m_current; + return false; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_MARKER_H diff --git a/src/libboardgame_base/Point.h b/src/libboardgame_base/Point.h new file mode 100644 index 0000000..f0e0a5e --- /dev/null +++ b/src/libboardgame_base/Point.h @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Point.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_POINT_H +#define LIBBOARDGAME_BASE_POINT_H + +#include +#include "libboardgame_util/Assert.h" +#include "libboardgame_sys/Compiler.h" + +namespace libboardgame_base { + +using namespace std; +using namespace libboardgame_util; + +//----------------------------------------------------------------------------- + +/** Coordinate on the board. + Depending on the game, a point represents a field or intersection (in Go) + on the board. The class is a lightweight wrapper around an integer. All + information about points including the coordinates are contained in + Geometry. The convention for the coordinates is that the top left corner of + the board has the coordinates (0,0). Point::null() has the meaning + "no point". + @tparam M The maximum number of on-board points of all geometries this + point is used in (excluding the null point). + @tparam W The maximum width of all geometries this point is used in. + @tparam H The maximum height of all geometries this point is used in. + @tparam I An unsigned integer type to store the point value. */ +template +class Point +{ +public: + typedef I IntType; + + static const unsigned max_onboard = M; + + static const unsigned max_width = W; + + static const unsigned max_height = W; + + static_assert(numeric_limits::is_integer, ""); + + static_assert(! numeric_limits::is_signed, ""); + + static_assert(max_onboard <= max_width * max_height, ""); + + static const unsigned range_onboard = max_onboard; + + static const unsigned range = max_onboard + 1; + + static Point null(); + + LIBBOARDGAME_FORCE_INLINE Point(); + + explicit Point(unsigned i); + + bool operator==(const Point& p) const; + + bool operator!=(const Point& p) const; + + bool operator<(const Point& p) const; + + bool is_null() const; + + /** Return point as an integer between 0 and Point::range */ + unsigned to_int() const; + +private: + static const IntType value_uninitialized = range; + + static const IntType value_null = range - 1; + + IntType m_i; + + LIBBOARDGAME_FORCE_INLINE bool is_initialized() const; +}; + +template +inline Point::Point() +{ +#if LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +template +inline Point::Point(unsigned i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = static_cast(i); +} + +template +inline bool Point::operator==(const Point& p) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(p.is_initialized()); + return m_i == p.m_i; +} + +template +inline bool Point::operator!=(const Point& p) const +{ + return ! operator==(p); +} + +template +inline bool Point::operator<(const Point& p) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(p.is_initialized()); + return m_i < p.m_i; +} + +template +inline bool Point::is_initialized() const +{ + return m_i < value_uninitialized; +} + +template +inline bool Point::is_null() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i == value_null; +} + +template +inline auto Point::null() -> Point +{ + return Point(value_null); +} + +template +inline unsigned Point::to_int() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_POINT_H diff --git a/src/libboardgame_base/PointTransform.h b/src/libboardgame_base/PointTransform.h new file mode 100644 index 0000000..e86545c --- /dev/null +++ b/src/libboardgame_base/PointTransform.h @@ -0,0 +1,429 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/PointTransform.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_POINT_TRANSFORM_H +#define LIBBOARDGAME_BASE_POINT_TRANSFORM_H + +#include +#include "Geometry.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +/** %Transform a point. + @tparam P An instance of class Point. */ +template +class PointTransform +{ +public: + typedef P Point; + + virtual ~PointTransform(); + + virtual Point get_transformed(const Point& p, + const Geometry

& geo) const = 0; +}; + +template +PointTransform

::~PointTransform() = default; + +//----------------------------------------------------------------------------- + +template +class PointTransfIdent + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfIdent

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + LIBBOARDGAME_UNUSED(geo); + return p; +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 90 degrees. */ +template +class PointTransfRot90 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRot90

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_width() - geo.get_y(p) - 1; + unsigned y = geo.get_x(p); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 180 degrees. */ +template +class PointTransfRot180 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRot180

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_width() - geo.get_x(p) - 1; + unsigned y = geo.get_height() - geo.get_y(p) - 1; + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 270 degrees. */ +template +class PointTransfRot270 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRot270

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_y(p); + unsigned y = geo.get_height() - geo.get_x(p) - 1; + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 270 degrees and reflect on y axis shifted to the center. + This is equivalent to a reflection on the x=y line. */ +template +class PointTransfRot270Refl + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRot270Refl

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + return geo.get_point(geo.get_y(p), geo.get_x(p)); +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 90 degrees and reflect on y axis shifted to the center. + This is equivalent to a reflection on the x=width-y line. */ +template +class PointTransfRot90Refl + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRot90Refl

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_width() - geo.get_y(p) - 1; + unsigned y = geo.get_height() - geo.get_x(p) - 1; + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +/** Mirror along x axis. */ +template +class PointTransfRefl + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRefl

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_width() - geo.get_x(p) - 1; + unsigned y = geo.get_y(p); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +/** Mirror along y axis. */ +template +class PointTransfReflRot180 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfReflRot180

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_x(p); + unsigned y = geo.get_height() - geo.get_y(p) - 1; + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot60 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonRot60

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx + 0.5f * px + 1.5f * py)); + unsigned y = static_cast(round(cy - 0.5f * px + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot120 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonRot120

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx - 0.5f * px + 1.5f * py)); + unsigned y = static_cast(round(cy - 0.5f * px - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot240 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonRot240

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx - 0.5f * px - 1.5f * py)); + unsigned y = static_cast(round(cy + 0.5f * px - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot300 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonRot300

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx + 0.5f * px - 1.5f * py)); + unsigned y = static_cast(round(cy + 0.5f * px + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot60 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonReflRot60

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx + 0.5f * (-px) + 1.5f * py)); + unsigned y = static_cast(round(cy - 0.5f * (-px) + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot120 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonReflRot120

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx - 0.5f * (-px) + 1.5f * py)); + unsigned y = static_cast(round(cy - 0.5f * (-px) - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot240 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonReflRot240

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx - 0.5f * (-px) - 1.5f * py)); + unsigned y = static_cast(round(cy + 0.5f * (-px) - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot300 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonReflRot300

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx + 0.5f * (-px) - 1.5f * py)); + unsigned y = static_cast(round(cy + 0.5f * (-px) + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_POINT_TRANSFORM_H diff --git a/src/libboardgame_base/Rating.cpp b/src/libboardgame_base/Rating.cpp new file mode 100644 index 0000000..9fd9136 --- /dev/null +++ b/src/libboardgame_base/Rating.cpp @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Rating.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Rating.h" + +#include +#include +#include "libboardgame_util/Assert.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const Rating& rating) +{ + out << rating.m_elo; + return out; +} + +istream& operator>>(istream& in, Rating& rating) +{ + in >> rating.m_elo; + return in; +} + +float Rating::get_expected_result(Rating elo_opponent, + unsigned nu_opponents) const +{ + float diff = elo_opponent.m_elo - m_elo; + return + 1.f + / (1.f + static_cast(nu_opponents) * pow(10.f, diff / 400.f)); +} + +void Rating::update(float game_result, Rating elo_opponent, float k_value, + unsigned nu_opponents) +{ + LIBBOARDGAME_ASSERT(k_value > 0); + float diff = game_result - get_expected_result(elo_opponent, nu_opponents); + m_elo += k_value * diff; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/Rating.h b/src/libboardgame_base/Rating.h new file mode 100644 index 0000000..8b190a7 --- /dev/null +++ b/src/libboardgame_base/Rating.h @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Rating.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_RATING_H +#define LIBBOARDGAME_BASE_RATING_H + +#include +#include + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Elo-rating of a player. */ +class Rating +{ +public: + friend ostream& operator<<(ostream& out, const Rating& rating); + friend istream& operator>>(istream& in, Rating& rating); + + explicit Rating(float elo = 0); + + /** Get the expected outcome of a game. + @param elo_opponent Elo-rating of the opponent. + @param nu_opponents The number of opponents (all with the same rating + elo_opponent) */ + float get_expected_result(Rating elo_opponent, + unsigned nu_opponents = 1) const; + + /** Update a rating after a game. + @param game_result The outcome of the game (0=loss, 0.5=tie, 1=win) + @param elo_opponent Elo-rating of the opponent. + @param k_value The K-value + @param nu_opponents The number of opponents (all with the same rating + elo_opponent) */ + void update(float game_result, Rating elo_opponent, float k_value = 32, + unsigned nu_opponents = 1); + + float get() const; + + /** Get rating rounded to an integer. */ + int to_int() const; + +private: + float m_elo; +}; + +inline Rating::Rating(float elo) + : m_elo(elo) +{ +} + +inline float Rating::get() const +{ + return m_elo; +} + +inline int Rating::to_int() const +{ + return static_cast(round(m_elo)); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_RATING_H diff --git a/src/libboardgame_base/RectGeometry.h b/src/libboardgame_base/RectGeometry.h new file mode 100644 index 0000000..ea47e0a --- /dev/null +++ b/src/libboardgame_base/RectGeometry.h @@ -0,0 +1,137 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/RectGeometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_RECT_GEOMETRY_H +#define LIBBOARDGAME_BASE_RECT_GEOMETRY_H + +#include +#include +#include "Geometry.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Geometry of a regular rectangular grid. + @tparam P An instantiation of libboardgame_base::Point */ +template +class RectGeometry final + : public Geometry

+{ +public: + typedef P Point; + + using AdjCoordList = typename Geometry

::AdjCoordList; + using DiagCoordList = typename Geometry

::DiagCoordList; + using AdjList = typename Geometry

::AdjList; + using DiagList = typename Geometry

::DiagList; + + /** Create or reuse an already created geometry with a given size. */ + static const RectGeometry& get(unsigned width, unsigned height); + + RectGeometry(unsigned width, unsigned height); + + AdjCoordList get_adj_coord(int x, int y) const override; + + DiagCoordList get_diag_coord(int x, int y) const override; + + unsigned get_point_type(int x, int y) const override; + + unsigned get_period_x() const override; + + unsigned get_period_y() const override; + +protected: + bool init_is_onboard(unsigned x, unsigned y) const override; + +private: + /** Stores already created geometries by width and height. */ + static map, shared_ptr> s_geometry; +}; + +template +map, shared_ptr>> + RectGeometry

::s_geometry; + +template +RectGeometry

::RectGeometry(unsigned width, unsigned height) +{ + Geometry

::init(width, height); +} + +template +const RectGeometry

& RectGeometry

::get(unsigned width, unsigned height) +{ + auto key = make_pair(width, height); + auto pos = s_geometry.find(key); + if (pos != s_geometry.end()) + return *pos->second; + auto geometry = make_shared(width, height); + return *s_geometry.insert(make_pair(key, geometry)).first->second; +} + +template +auto RectGeometry

::get_adj_coord(int x, int y) const -> AdjCoordList +{ + AdjCoordList l; + l.push_back(CoordPoint(x, y - 1)); + l.push_back(CoordPoint(x - 1, y)); + l.push_back(CoordPoint(x + 1, y)); + l.push_back(CoordPoint(x, y + 1)); + return l; +} + +template +auto RectGeometry

::get_diag_coord(int x, int y) const -> DiagCoordList +{ + // The order does not matter logically but it is better to put far away + // points first because in Blokus, libpentobi::BoardConst uses the + // forbidden status of the first points during move generation and far away + // points can reject more moves. + DiagCoordList l; + l.push_back(CoordPoint(x - 1, y - 1)); + l.push_back(CoordPoint(x + 1, y + 1)); + l.push_back(CoordPoint(x + 1, y - 1)); + l.push_back(CoordPoint(x - 1, y + 1)); + return l; +} + +template +unsigned RectGeometry

::get_period_x() const +{ + return 1; +} + +template +unsigned RectGeometry

::get_period_y() const +{ + return 1; +} + +template +unsigned RectGeometry

::get_point_type(int x, int y) const +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return 0; +} + +template +bool RectGeometry

::init_is_onboard(unsigned x, unsigned y) const +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return true; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_RECT_GEOMETRY_H diff --git a/src/libboardgame_base/RectTransform.cpp b/src/libboardgame_base/RectTransform.cpp new file mode 100644 index 0000000..5553da4 --- /dev/null +++ b/src/libboardgame_base/RectTransform.cpp @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/RectTransform.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RectTransform.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +CoordPoint TransfIdentity::get_transformed(const CoordPoint& p) const +{ + return p; +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot90::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.y, p.x); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot180::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.x, -p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot270::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(p.y, -p.x); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRefl::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.x, p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot90Refl::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.y, -p.x); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot180Refl::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(p.x, -p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot270Refl::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(p.y, p.x); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/RectTransform.h b/src/libboardgame_base/RectTransform.h new file mode 100644 index 0000000..823de62 --- /dev/null +++ b/src/libboardgame_base/RectTransform.h @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/RectTransform.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_RECTTRANSFORM_H +#define LIBBOARDGAME_BASE_RECTTRANSFORM_H + +#include "Transform.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +class TransfIdentity + : public Transform +{ +public: + TransfIdentity() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot90 + : public Transform +{ +public: + TransfRectRot90() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot180 + : public Transform +{ +public: + TransfRectRot180() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot270 + : public Transform +{ +public: + TransfRectRot270() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRefl + : public Transform +{ +public: + TransfRectRefl() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot90Refl + : public Transform +{ +public: + TransfRectRot90Refl() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot180Refl + : public Transform +{ +public: + TransfRectRot180Refl() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot270Refl + : public Transform +{ +public: + TransfRectRot270Refl() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_TRANSFORM_H diff --git a/src/libboardgame_base/StringRep.cpp b/src/libboardgame_base/StringRep.cpp new file mode 100644 index 0000000..4063939 --- /dev/null +++ b/src/libboardgame_base/StringRep.cpp @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/StringRep.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "StringRep.h" + +#include +#include +#include "libboardgame_util/StringUtil.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_base { + +using libboardgame_util::get_letter_coord; + +//----------------------------------------------------------------------------- + +StringRep::~StringRep() = default; + +//----------------------------------------------------------------------------- + +StdStringRep::~StdStringRep() = default; + +bool StdStringRep::read(istream& in, unsigned width, unsigned height, + unsigned& x, unsigned& y) const +{ + int c; + while (true) + { + c = in.peek(); + if (c == EOF || ! isspace(c)) + break; + in.get(); + } + bool read_x = false; + x = 0; + while (true) + { + c = in.peek(); + if (c == EOF || ! isalpha(c)) + break; + c = tolower(in.get()); + if (c < 'a' || c > 'z') + return false; + x = 26 * x + (c - 'a' + 1); + read_x = true; + } + if (! read_x) + return false; + --x; + if (x >= width) + return false; + c = in.peek(); + if (c < '0' || c > '9') + return false; + in >> y; + if (! in || y > height + 1) + return false; + y = height - y; + c = in.peek(); + if (c == EOF) + { + in.clear(); + return true; + } + if (isspace(c)) + return true; + return false; +} + +void StdStringRep::write(ostream& out, unsigned x, unsigned y, unsigned width, + unsigned height) const +{ + LIBBOARDGAME_UNUSED(width); + out << get_letter_coord(x) << (height - y); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/StringRep.h b/src/libboardgame_base/StringRep.h new file mode 100644 index 0000000..db6a674 --- /dev/null +++ b/src/libboardgame_base/StringRep.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/StringRep.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_STRING_REP_H +#define LIBBOARDGAME_BASE_STRING_REP_H + +#include + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** String representation of points. */ +struct StringRep +{ + virtual ~StringRep(); + + virtual bool read(istream& in, unsigned width, unsigned height, + unsigned& x, unsigned& y) const = 0; + + virtual void write(ostream& out, unsigned x, unsigned y, unsigned width, + unsigned height) const = 0; +}; + +//----------------------------------------------------------------------------- + +/** Spreadsheet-style string representation of points. + Can be used as a template argument for libboardgame_base::Point. + Columns are represented as letters including the letter 'J'. After 'Z', + multi-letter combinations are used: 'AA', 'AB', etc. Rows are represented + by numbers starting with '1'. Note that unlike in spreadsheets, row number + 1 is at the bottom and increases to the top to be compatible with the + convention used in chess. */ +struct StdStringRep + : public StringRep +{ + ~StdStringRep(); + + bool read(istream& in, unsigned width, unsigned height, unsigned& x, + unsigned& y) const override; + + void write(ostream& out, unsigned x, unsigned y, unsigned width, + unsigned height) const override; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_STRING_REP_H diff --git a/src/libboardgame_base/Transform.cpp b/src/libboardgame_base/Transform.cpp new file mode 100644 index 0000000..b07cf76 --- /dev/null +++ b/src/libboardgame_base/Transform.cpp @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Transform.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Transform.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +Transform::~Transform() +{ +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/Transform.h b/src/libboardgame_base/Transform.h new file mode 100644 index 0000000..8e170dc --- /dev/null +++ b/src/libboardgame_base/Transform.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Transform.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_TRANSFORM_H +#define LIBBOARDGAME_BASE_TRANSFORM_H + +#include "CoordPoint.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +/** Rotation and/or reflection of local coordinates on the board. */ +class Transform +{ +public: + virtual ~Transform(); + + virtual CoordPoint get_transformed(const CoordPoint& p) const = 0; + + /** Get the new point type of the (0,0) coordinates. + The transformation may change the point type of the (0,0) coordinates. + For example, in the Blokus Trigon board, a reflection at the y axis + changes the type from 0 (=downside triangle) to 1 (=upside triangle). + @see Geometry::get_point_type() */ + unsigned get_new_point_type() const { return m_new_point_type; } + + /** @tparam I An iterator of a container with elements of type CoordPoint */ + template + void transform(I begin, I end) const; + +protected: + explicit Transform(unsigned new_point_type) + : m_new_point_type(new_point_type) + {} + +private: + unsigned m_new_point_type; +}; + +template +void Transform::transform(I begin, I end) const +{ + for (I i = begin; i != end; ++i) + *i = get_transformed(*i); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_TRANSFORM_H diff --git a/src/libboardgame_gtp/Arguments.cpp b/src/libboardgame_gtp/Arguments.cpp new file mode 100644 index 0000000..019a619 --- /dev/null +++ b/src/libboardgame_gtp/Arguments.cpp @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Arguments.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Arguments.h" + +#include + +namespace libboardgame_gtp { + +//----------------------------------------------------------------------------- + +void Arguments::check_size(unsigned n) const +{ + if (get_size() == n) + return; + if (n == 0) + throw Failure("no arguments allowed"); + else if (n == 1) + throw Failure("command needs one argument"); + else + { + ostringstream msg; + msg << "command needs " << n << " arguments"; + throw Failure(msg.str()); + } +} + +void Arguments::check_size_less_equal(unsigned n) const +{ + if (get_size() <= n) + return; + if (n == 1) + throw Failure("command needs at most one argument"); + else + { + ostringstream msg; + msg << "command needs at most " << n << " arguments"; + throw Failure(msg.str()); + } +} + +CmdLineRange Arguments::get(unsigned i) const +{ + if (i < get_size()) + return m_line.get_element(m_line.get_idx_name() + i + 1); + ostringstream msg; + msg << "missing argument " << (i + 1); + throw Failure(msg.str()); +} + +string Arguments::get_tolower(unsigned i) const +{ + string value = get(i); + for (auto& c : value) + c = static_cast(tolower(c)); + return value; +} + +string Arguments::get_tolower() const +{ + check_size(1); + return get_tolower(0); +} + +CmdLineRange Arguments::get_remaining_line(unsigned i) const +{ + if (i < get_size()) + return m_line.get_trimmed_line_after_elem(m_line.get_idx_name() + i + + 1); + ostringstream msg; + msg << "missing argument " << (i + 1); + throw Failure(msg.str()); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp diff --git a/src/libboardgame_gtp/Arguments.h b/src/libboardgame_gtp/Arguments.h new file mode 100644 index 0000000..8a4bc4a --- /dev/null +++ b/src/libboardgame_gtp/Arguments.h @@ -0,0 +1,240 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Arguments.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_ARGUMENTS_H +#define LIBBOARDGAME_GTP_ARGUMENTS_H + +#ifdef __GNUC__ +#include +#endif +#include +#include "CmdLine.h" +#include "Failure.h" + +namespace libboardgame_gtp { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Access arguments of command line. */ +class Arguments +{ +public: + /** Constructor. + @param line The command line (@ref libboardgame_doc_storesref) */ + explicit Arguments(const CmdLine& line); + + /** Get argument. + @param i Argument index starting with 0 + @return Argument value + @throws Failure If no such argument */ + CmdLineRange get(unsigned i) const; + + /** Get single argument. + @return Argument value + @throws Failure If no such argument or command has more than one + arguments */ + CmdLineRange get() const; + + /** Get argument converted to lowercase. + @param i Argument index starting with 0 + @return Copy of argument value converted to lowercase + @throws Failure If no such argument */ + string get_tolower(unsigned i) const; + + /** Get single argument converted to lowercase. */ + string get_tolower() const; + + /** Get argument converted to a type. + The type must implement operator<<(istream) + @param i Argument index starting with 0 + @return The converted argument + @throws Failure If no such argument, or argument cannot be converted */ + template + T parse(unsigned i) const; + + /** Get single argument converted to a type. + The type must implement operator<<(istream) + @return The converted argument + @throws Failure If no such argument, or argument cannot be converted, + or command has more than one arguments */ + template + T parse() const; + + /** Get argument converted to a type and check against a minum value. + The type must implement operator<< and operator< + @param i Argument index starting with 0 + @param min Minimum allowed value + @return Argument value + @throws Failure If no such argument, argument cannot be converted + or smaller than the mimimum value */ + template + T parse_min(unsigned i, T min) const; + + /** Get argument converted to a type and check against a range. + The type must implement operator<< and operator< + @param i Argument index starting with 0 + @param min Minimum allowed value + @param max Maximum allowed value + @return Argument value + @throws Failure If no such argument, argument cannot be converted + or not in range */ + template + T parse_min_max(unsigned i, T min, T max) const; + + template + T parse_min_max(T min, T max) const; + + /** Check that command has no arguments. + @throws Failure If command has arguments + */ + void check_empty() const; + + /** Check number of arguments. + @param n Expected number of arguments + @throws Failure If command has a different number of arguments */ + void check_size(unsigned n) const; + + /** Check maximum number of arguments. + @param n Expected maximum number of arguments + @throws Failure If command has more arguments */ + void check_size_less_equal(unsigned n) const; + + /** Get argument line. + Get all arguments as a line. + No modfications to the line were made apart from trimmimg leading + and trailing white spaces. */ + CmdLineRange get_line() const; + + /** Get number of arguments. */ + unsigned get_size() const; + + /** Return remaining line after argument. + @param i Argument index starting with 0 + @return The remaining line after the given argument, unmodified apart + from leading and trailing whitespaces, which are trimmed. Quotation + marks are not handled. + @throws Failure If no such argument */ + CmdLineRange get_remaining_line(unsigned i) const; + +private: + const CmdLine& m_line; + + template + static string get_type_name(); +}; + +inline Arguments::Arguments(const CmdLine& line) + : m_line(line) +{ +} + +inline void Arguments::check_empty() const +{ + check_size(0); +} + +inline CmdLineRange Arguments::get() const +{ + check_size(1); + return get(0); +} + +inline CmdLineRange Arguments::get_line() const +{ + return m_line.get_trimmed_line_after_elem(m_line.get_idx_name()); +} + +inline unsigned Arguments::get_size() const +{ + return + static_cast(m_line.get_elements().size()) + - m_line.get_idx_name() - 1; +} + +template +string Arguments::get_type_name() +{ +#ifdef __GNUC__ + int status; + auto name_ptr = + abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, &status); + if (status == 0) + { + string result(name_ptr); + free(name_ptr); + return result; + } + else + return typeid(T).name(); +#else + return typeid(T).name(); +#endif +} + +template +T Arguments::parse() const +{ + check_size(1); + return parse(0); +} + +template +T Arguments::parse(unsigned i) const +{ + string s = get(i); + istringstream in(s); + T result; + in >> result; + if (! in) + { + ostringstream msg; + msg << "argument " << (i + 1) << " ('" << s + << "') has invalid type (expected " << get_type_name() << ")"; + throw Failure(msg.str()); + } + return result; +} + +template +T Arguments::parse_min(unsigned i, T min) const +{ + T result = parse(i); + if (result < min) + { + ostringstream msg; + msg << "argument " << (i + 1) << " must be greater or equal " << min; + throw Failure(msg.str()); + } + return result; +} + +template +T Arguments::parse_min_max(T min, T max) const +{ + check_size(1); + return parse_min_max(0, min, max); +} + +template +T Arguments::parse_min_max(unsigned i, T min, T max) const +{ + T result = parse_min(i, min); + if (max < result) + { + ostringstream msg; + msg << "argument " << (i + 1) << " must be less or equal " << max; + throw Failure(msg.str()); + } + return result; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_ARGUMENTS_H diff --git a/src/libboardgame_gtp/CMakeLists.txt b/src/libboardgame_gtp/CMakeLists.txt new file mode 100644 index 0000000..fea26bd --- /dev/null +++ b/src/libboardgame_gtp/CMakeLists.txt @@ -0,0 +1,12 @@ +add_library(boardgame_gtp STATIC + Arguments.h + Arguments.cpp + CmdLine.h + CmdLine.cpp + CmdLineRange.h + Engine.h + Engine.cpp + Failure.h + Response.h + Response.cpp +) diff --git a/src/libboardgame_gtp/CmdLine.cpp b/src/libboardgame_gtp/CmdLine.cpp new file mode 100644 index 0000000..7fe7f5f --- /dev/null +++ b/src/libboardgame_gtp/CmdLine.cpp @@ -0,0 +1,116 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/CmdLine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "CmdLine.h" + +#include +#include +#include + +namespace libboardgame_gtp { + +//----------------------------------------------------------------------------- + +CmdLine::~CmdLine() = default; + +void CmdLine::add_elem(string::const_iterator begin, + string::const_iterator end) +{ + // Ignore command line elements greater UINT_MAX because we use unsigned + // for element indices. + if (m_elem.size() < numeric_limits::max()) + m_elem.emplace_back(begin, end); +} + +/** Find elements (ID, command name, arguments). + Arguments are words separated by whitespaces. + Arguments with whitespaces can be quoted with quotation marks ('"'). + Characters can be escaped with a backslash ('\'). */ +void CmdLine::find_elem() +{ + m_elem.clear(); + bool escape = false; + bool is_in_string = false; + string::const_iterator begin = m_line.begin(); + string::const_iterator i; + for (i = begin; i < m_line.end(); ++i) + { + char c = *i; + if (c == '"' && ! escape) + { + if (is_in_string) + add_elem(begin, i); + begin = i + 1; + is_in_string = ! is_in_string; + } + else if (isspace(static_cast(c)) && ! is_in_string) + { + if (i > begin) + m_elem.emplace_back(begin, i); + begin = i + 1; + } + escape = (c == '\\' && ! escape); + } + if (i > begin) + m_elem.emplace_back(begin, m_line.end()); +} + +CmdLineRange CmdLine::get_trimmed_line_after_elem(unsigned i) const +{ + assert(i < m_elem.size()); + auto& e = m_elem[i]; + auto begin = e.end(); + if (begin < m_line.end() && *begin == '"') + ++begin; + while (begin < m_line.end() && isspace(static_cast(*begin))) + ++begin; + auto end = m_line.end(); + while (end > begin && isspace(static_cast(*(end - 1)))) + --end; + return CmdLineRange(begin, end); +} + +void CmdLine::init(const string& line) +{ + m_line = line; + find_elem(); + assert(! m_elem.empty()); + parse_id(); + assert(! m_elem.empty()); +} + +void CmdLine::init(const CmdLine& c) +{ + m_idx_name = c.m_idx_name; + m_line = c.m_line; + m_elem.clear(); + for (auto& i : c.m_elem) + { + auto begin = m_line.begin() + (i.begin() - c.m_line.begin()); + auto end = m_line.begin() + (i.end() - c.m_line.begin()); + m_elem.emplace_back(begin, end); + } +} + +void CmdLine::parse_id() +{ + m_idx_name = 0; + if (m_elem.size() < 2) + return; + istringstream in(m_elem[0]); + int id; + in >> id; + if (in) + m_idx_name = 1; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp diff --git a/src/libboardgame_gtp/CmdLine.h b/src/libboardgame_gtp/CmdLine.h new file mode 100644 index 0000000..c51b966 --- /dev/null +++ b/src/libboardgame_gtp/CmdLine.h @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/CmdLine.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_CMDLINE_H +#define LIBBOARDGAME_GTP_CMDLINE_H + +#include +#include +#include +#include +#include +#include "CmdLineRange.h" + +namespace libboardgame_gtp { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Parsed GTP command line. + Only used internally by libboardgame_gtp::Engine. GTP command handlers + query arguments of the command line through the instance of class Arguments + given as a function argument by class Engine to the command handler. */ +class CmdLine +{ +public: + /** Construct empty command. + @warning An empty command cannot be used, before init() was called. + This constructor exists only to reuse instances. */ + CmdLine() = default; + + /** Construct with a command line. + @see init() */ + explicit CmdLine(const string& line); + + ~CmdLine(); + + void init(const string& line); + + void init(const CmdLine& c); + + const string& get_line() const; + + /** Get command name. */ + CmdLineRange get_name() const; + + void write_id(ostream& out) const; + + CmdLineRange get_trimmed_line_after_elem(unsigned i) const; + + const vector& get_elements() const; + + const CmdLineRange& get_element(unsigned i) const; + + int get_idx_name() const; + +private: + int m_idx_name; + + /** Full command line. */ + string m_line; + + vector m_elem; + + void add_elem(string::const_iterator begin, string::const_iterator end); + + void find_elem(); + + void parse_id(); +}; + +inline CmdLine::CmdLine(const string& line) +{ + init(line); +} + +inline const vector& CmdLine::get_elements() const +{ + return m_elem; +} + +inline const CmdLineRange& CmdLine::get_element(unsigned i) const +{ + assert(i < m_elem.size()); + return m_elem[i]; +} + +inline int CmdLine::get_idx_name() const +{ + return m_idx_name; +} + +inline const string& CmdLine::get_line() const +{ + return m_line; +} + +inline CmdLineRange CmdLine::get_name() const +{ + return m_elem[m_idx_name]; +} + +inline void CmdLine::write_id(ostream& out) const +{ + if (m_idx_name == 0) + return; + auto& e = m_elem[0]; + copy(e.begin(), e.end(), ostream_iterator(out)); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_CMDLINE_H diff --git a/src/libboardgame_gtp/CmdLineRange.h b/src/libboardgame_gtp/CmdLineRange.h new file mode 100644 index 0000000..24ef9ba --- /dev/null +++ b/src/libboardgame_gtp/CmdLineRange.h @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/CmdLineRange.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_CMDLINERANGE_H +#define LIBBOARDGAME_GTP_CMDLINERANGE_H + +#include +#include +#include + +namespace libboardgame_gtp { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Subrange of the GTP command line. + Avoids allocation of strings on the heap for each parsed command line. + Instances of this class are valid only during the lifetime of the command + line object. Command handlers, which access the command line through the + instance of Arguments given as a function argument, should not store + references to CmdLineRange objects. */ +struct CmdLineRange +{ + string::const_iterator m_begin; + + string::const_iterator m_end; + + + CmdLineRange(string::const_iterator begin, string::const_iterator end) + : m_begin(begin), + m_end(end) + { } + + bool operator==(const string& s) const + { + return size() == s.size() && equal(m_begin, m_end, s.begin()); + } + + bool operator!=(const string& s) const + { + return ! operator==(s); + } + + operator string() const + { + return string(m_begin, m_end); + } + + string::const_iterator begin() const + { + return m_begin; + } + + string::const_iterator end() const + { + return m_end; + } + + string::size_type size() const + { + return m_end - m_begin; + } + + void write(ostream& o) const + { + o << string(*this); + } +}; + +//----------------------------------------------------------------------------- + +inline ostream& operator<<(ostream& out, const CmdLineRange& r) +{ + r.write(out); + return out; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_CMDLINERANGE_H diff --git a/src/libboardgame_gtp/Engine.cpp b/src/libboardgame_gtp/Engine.cpp new file mode 100644 index 0000000..f0e4e04 --- /dev/null +++ b/src/libboardgame_gtp/Engine.cpp @@ -0,0 +1,243 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Engine.h" + +#include +#include +#include "CmdLine.h" + +namespace libboardgame_gtp { + +//----------------------------------------------------------------------------- + +/** Utility functions. */ +namespace { + +/** Check, if line contains a command. */ +bool is_cmd_line(const string& line) +{ + for (char c : line) + if (! isspace(static_cast(c))) + return c != '#'; + return false; +} + +/** Read next command from stream. + @param in The input stream. + @param[out] c The command (reused for efficiency) + @return @c false on end-of-stream or read error. */ +bool read_cmd(CmdLine& c, istream& in) +{ + string line; + while (getline(in, line)) + if (is_cmd_line(line)) + break; + if (! in.fail()) + { + c.init(line); + return true; + } + else + return false; +} + +} // namespace + +//----------------------------------------------------------------------------- + +Engine::Engine() +{ + add("known_command", &Engine::cmd_known_command); + add("list_commands", &Engine::cmd_list_commands); + add("name", &Engine::cmd_name); + add("protocol_version", &Engine::cmd_protocol_version); + add("quit", &Engine::cmd_quit); + add("version", &Engine::cmd_version); +} + +Engine::~Engine() = default; + +void Engine::add(const string& name, Handler f) +{ + m_handlers[name] = f; +} + +void Engine::add(const string& name, HandlerNoArgs f) +{ + add(name, + Handler(bind(no_args_wrapper, f, placeholders::_1, placeholders::_2))); +} + +void Engine::add(const string& name, HandlerNoResponse f) +{ + add(name, Handler(bind(no_response_wrapper, f, + placeholders::_1, placeholders::_2))); +} + +void Engine::add(const string& name, HandlerNoArgsNoResponse f) +{ + add(name, Handler(bind(no_args_no_response_wrapper, f, + placeholders::_1, placeholders::_2))); +} + +/** Return @c true if command is known, @c false otherwise. */ +void Engine::cmd_known_command(const Arguments& args, Response& response) +{ + response.set(contains(args.get()) ? "true" : "false"); +} + +/** List all known commands. */ +void Engine::cmd_list_commands(Response& response) +{ + for (auto& i : m_handlers) + response << i.first << '\n'; +} + +/** Return name. */ +void Engine::cmd_name(Response& response) +{ + response.set("Unknown"); +} + +/** Return protocol version. */ +void Engine::cmd_protocol_version(Response& response) +{ + response.set("2"); +} + +/** Quit command loop. */ +void Engine::cmd_quit() +{ + m_quit = true; +} + +/** Return empty version string. + The GTP standard says to return empty string, if no meaningful response + is available. */ +void Engine::cmd_version(Response&) +{ +} + +bool Engine::contains(const string& name) const +{ + return m_handlers.count(name) > 0; +} + +bool Engine::exec(istream& in, bool throw_on_fail, ostream* log) +{ + string line; + Response response; + string buffer; + CmdLine cmd; + while (getline(in, line)) + { + if (! is_cmd_line(line)) + continue; + cmd.init(line); + if (log) + *log << cmd.get_line() << '\n'; + bool status = handle_cmd(cmd, log, response, buffer); + if (! status && throw_on_fail) + { + ostringstream msg; + msg << "executing '" << cmd.get_line() << "' failed"; + throw Failure(msg.str()); + } + } + return ! in.fail(); +} + +void Engine::exec_main_loop(istream& in, ostream& out) +{ + m_quit = false; + CmdLine cmd; + Response response; + string buffer; + while (! m_quit) + { + if (read_cmd(cmd, in)) + handle_cmd(cmd, &out, response, buffer); + else + break; + } +} + +/** Call the handler of a command and write its response. + @param line The command + @param out The output stream for the response + @param response A reusable response instance to avoid memory allocation in + each function call + @param buffer A reusable string instance to avoid memory allocation in each + function call */ +bool Engine::handle_cmd(CmdLine& line, ostream* out, Response& response, + string& buffer) +{ + on_handle_cmd_begin(); + bool status = true; + try + { + response.clear(); + auto pos = m_handlers.find(line.get_name()); + if (pos != m_handlers.end()) + { + Arguments args(line); + (pos->second)(args, response); + } + else + { + status = false; + response << "unknown command (" << line.get_name() << ')'; + } + } + catch (const Failure& failure) + { + status = false; + response.set(failure.what()); + } + if (out) + { + *out << (status ? '=' : '?'); + line.write_id(*out); + *out << ' '; + response.write(*out, buffer); + out->flush(); + } + return status; +} + +void Engine::no_args_wrapper(HandlerNoArgs h, const Arguments& args, + Response& response) +{ + args.check_empty(); + h(response); +} + +void Engine::no_response_wrapper(HandlerNoResponse h, const Arguments& args, + Response&) +{ + h(args); +} + +void Engine::no_args_no_response_wrapper(HandlerNoArgsNoResponse h, + const Arguments& args, Response&) +{ + args.check_empty(); + h(); +} + +void Engine::on_handle_cmd_begin() +{ + // Default implementation does nothing +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp diff --git a/src/libboardgame_gtp/Engine.h b/src/libboardgame_gtp/Engine.h new file mode 100644 index 0000000..b043164 --- /dev/null +++ b/src/libboardgame_gtp/Engine.h @@ -0,0 +1,227 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Engine.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_ENGINE_H +#define LIBBOARDGAME_GTP_ENGINE_H + +#include +#include +#include +#include "Arguments.h" +#include "Response.h" + +namespace libboardgame_gtp { + +class CmdLine; + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Base class for GTP engines. + Commands can be added with Engine::add(). Existing commands can be + overridden by registering a new handler for the command. + @see @ref libboardgame_gtp_commands */ +class Engine +{ +public: + typedef function Handler; + + typedef function HandlerNoArgs; + + typedef function HandlerNoResponse; + + typedef function HandlerNoArgsNoResponse; + + /** @page libboardgame_gtp_commands libboardgame_gtp::Engine GTP commands +

+
@link cmd_known_command() @c known_command @endlink
+
@copydoc cmd_known_command()
+
@link cmd_list_commands() @c list_commands @endlink
+
@copydoc cmd_list_commands()
+
@link cmd_name() @c name @endlink
+
@copydoc cmd_name()
+
@link cmd_protocol_version() @c protocol_version @endlink
+
@copydoc cmd_protocol_version()
+
@link cmd_quit() @c quit @endlink
+
@copydoc cmd_quit()
+
@link cmd_version() @c version @endlink
+
@copydoc cmd_version()
+
*/ + /** @name Command handlers */ + /** @{ */ + void cmd_known_command(const Arguments&, Response&); + void cmd_list_commands(Response&); + void cmd_name(Response&); + void cmd_protocol_version(Response&); + void cmd_quit(); + void cmd_version(Response&); + /** @} */ // @name + + Engine(); + + Engine(const Engine&) = delete; + + Engine& operator=(const Engine&) const = delete; + + virtual ~Engine(); + + /** Execute commands from an input stream. + @param in The input stream + @param throw_on_fail Whether to throw an exception if a command fails, + or to continue executing the remainign commands + @param log Stream for logging the commands and responses to. + @return The stream state as a bool + @throws Failure If a command fails, and @c throw_on_fail is @c true */ + bool exec(istream& in, bool throw_on_fail, ostream* log); + + /** Run the main command loop. + Reads lines from input stream, calls the corresponding command handler + and writes the response to the output stream. Empty lines in the + command responses will be replaced by a line containing a single space, + because empty lines are not allowed in GTP responses. */ + void exec_main_loop(istream& in, ostream& out); + + /** Register command handler. + If a command was already registered with the same name, it will be + replaced by the new command. */ + void add(const string& name, Handler f); + + void add(const string& name, HandlerNoArgs f); + + void add(const string& name, HandlerNoResponse f); + + void add(const string& name, HandlerNoArgsNoResponse f); + + /** Register a member function as a command handler. + If a command was already registered with the same name, it will be + replaced by the new command. */ + template + void add(const string& name, + void (T::*f)(const Arguments&, Response&), T* t); + + template + void add(const string& name, void (T::*f)(const Arguments&), T* t); + + template + void add(const string& name, void (T::*f)(Response&), T* t); + + template + void add(const string& name, void (T::*f)(), T* t); + + /** Returns if command registered. */ + bool contains(const string& name) const; + +protected: + /** Hook function to be executed before each command. + The default implementation does nothing. */ + virtual void on_handle_cmd_begin(); + + /** Register a member function of the current instance as a command + handler. + If a command was already registered with the same name, it will be + replaced by the new command. */ + template + void add(const string& name, void (T::*f)(const Arguments&, Response&)); + + template + void add(const string& name, void (T::*f)(const Arguments&)); + + template + void add(const string& name, void (T::*f)(Response&)); + + template + void add(const string& name, void (T::*f)()); + +private: + /** Mapping of command name to command handler. + They key is a string subrange, not a string, to allow looking up the + command name using Command::name_as_subrange() without creating a + temporary string for the command name. The value of type CmdInfo with + the name string and callback function are stored in an object allocated + on the heap to ensure that the range stays valid, if the value object + is copied. */ + typedef map Handlers; + + + /** Flag to quit main loop. */ + bool m_quit; + + Handlers m_handlers; + + + bool handle_cmd(CmdLine& line, ostream* out, Response& response, + string& buffer); + + static void no_args_wrapper(HandlerNoArgs h, + const Arguments& args, Response& response); + + static void no_response_wrapper(HandlerNoResponse h, + const Arguments& args, Response&); + + static void no_args_no_response_wrapper(HandlerNoArgsNoResponse h, + const Arguments& args, Response&); +}; + +template +void Engine::add(const string& name, void (T::*f)(const Arguments&, Response&)) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, void (T::*f)(Response&)) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, void (T::*f)(const Arguments&)) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, void (T::*f)()) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, + void (T::*f)(const Arguments&, Response&), T* t) +{ + assert(f); + add(name, + static_cast(bind(f, t, placeholders::_1, placeholders::_2))); +} + +template +void Engine::add(const string& name, void (T::*f)(Response&), T* t) +{ + assert(f); + add(name, static_cast(bind(f, t, placeholders::_1))); +} + +template +void Engine::add(const string& name, void (T::*f)(const Arguments&), T* t) +{ + assert(f); + add(name, static_cast(bind(f, t, placeholders::_1))); +} + +template +void Engine::add(const string& name, void (T::*f)(), T* t) +{ + assert(f); + add(name, static_cast(bind(f, t))); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_ENGINE_H diff --git a/src/libboardgame_gtp/Failure.h b/src/libboardgame_gtp/Failure.h new file mode 100644 index 0000000..90eee8a --- /dev/null +++ b/src/libboardgame_gtp/Failure.h @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Failure.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_FAILURE_H +#define LIBBOARDGAME_GTP_FAILURE_H + +#include + +namespace libboardgame_gtp { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** GTP failure. + Command handlers generate a GTP error response by throwing an instance + of Failure. */ +class Failure + : public runtime_error +{ + using runtime_error::runtime_error; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_ENGINE_H diff --git a/src/libboardgame_gtp/Response.cpp b/src/libboardgame_gtp/Response.cpp new file mode 100644 index 0000000..5868b3e --- /dev/null +++ b/src/libboardgame_gtp/Response.cpp @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Response.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Response.h" + +namespace libboardgame_gtp { + +//----------------------------------------------------------------------------- + +ostringstream Response::s_dummy; + +Response::~Response() = default; + +void Response::write(ostream& out, string& buffer) const +{ + buffer = m_stream.str(); + bool was_newline = false; + for (auto c : buffer) + { + bool is_newline = (c == '\n'); + if (is_newline && was_newline) + out << ' '; + out << c; + was_newline = is_newline; + } + if (! was_newline) + out << '\n'; + out << '\n'; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp diff --git a/src/libboardgame_gtp/Response.h b/src/libboardgame_gtp/Response.h new file mode 100644 index 0000000..2805818 --- /dev/null +++ b/src/libboardgame_gtp/Response.h @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Response.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_RESPONSE_H +#define LIBBOARDGAME_GTP_RESPONSE_H + +#include +#include + +namespace libboardgame_gtp { + +using namespace std; + +//----------------------------------------------------------------------------- + +class Response +{ +public: + ~Response(); + + /** Conversion to output stream. + Returns reference to response stream. */ + operator ostream&(); + + /** Get response. + @return A copy of the internal response string stream */ + string to_string() const; + + /** Set response. */ + void set(const string& response); + + void clear(); + + /** Write response to output stream. + Also sanitizes responses containing empty lines ("\n\n" cannot occur + in a response, because it means end of response; it will be replaced by + "\n \n") and adds "\n\n" add the end of the response. */ + void write(ostream& out, string& buffer) const; + +private: + /** Dummy stream for copying default formatting settings. */ + static ostringstream s_dummy; + + /** Response stream */ + ostringstream m_stream; +}; + +inline Response::operator ostream&() +{ + return m_stream; +} + +inline void Response::clear() +{ + m_stream.str(""); + m_stream.copyfmt(s_dummy); +} + +inline string Response::to_string() const +{ + return m_stream.str(); +} + +inline void Response::set(const string& response) +{ + m_stream.str(response); +} + +//----------------------------------------------------------------------------- + +/** @relates libboardgame_gtp::Response */ +template +inline Response& operator<<(Response& r, const TYPE& t) +{ + static_cast(r) << t; + return r; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_RESPONSE_H diff --git a/src/libboardgame_mcts/Atomic.h b/src/libboardgame_mcts/Atomic.h new file mode 100644 index 0000000..bf6bdb8 --- /dev/null +++ b/src/libboardgame_mcts/Atomic.h @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/Atomic.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_ATOMIC_H +#define LIBBOARDGAME_MCTS_ATOMIC_H + +#include +#include "libboardgame_util/Unused.h" + +namespace libboardgame_mcts { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Data that may be atomic. + This struct is used for sharing the same code for a single-threaded and + a multi-threaded implementation depending on a template argument. + In the multi-threaded implementation, the variable is atomic, which + usually causes a small performance penalty, in the single-threaded + implementation, it is simply a regular variable. + @param T The type of the variable. + @param MT true, if the variable should be atomic. */ +template struct Atomic; + +template +struct Atomic +{ + T val; + + T operator=(T t) + { + val = t; + return val; + } + + T load(memory_order order = memory_order_seq_cst) const + { + LIBBOARDGAME_UNUSED(order); + return val; + } + + void store(T t, memory_order order = memory_order_seq_cst) + { + LIBBOARDGAME_UNUSED(order); + val = t; + } + + operator T() const + { + return val; + } + + T fetch_add(T t) + { + T tmp = val; + val += t; + return tmp; + } +}; + +template +struct Atomic +{ + atomic val; + + T operator=(T t) + { + val.store(t); + return val; + } + + T load(memory_order order = memory_order_seq_cst) const + { + return val.load(order); + } + + void store(T t, memory_order order = memory_order_seq_cst) + { + val.store(t, order); + } + + operator T() const + { + return load(); + } + + T fetch_add(T t) + { + return val.fetch_add(t); + } +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_ATOMIC_H diff --git a/src/libboardgame_mcts/CMakeLists.txt b/src/libboardgame_mcts/CMakeLists.txt new file mode 100644 index 0000000..e0b4461 --- /dev/null +++ b/src/libboardgame_mcts/CMakeLists.txt @@ -0,0 +1,11 @@ +# This library contains only header files with templates. The empty target +# exists only to add the headers to IDE project files. +add_custom_target(boardgame_mcts SOURCES + Atomic.h + LastGoodReply.h + Node.h + PlayerMove.h + SearchBase.h + Tree.h + TreeUtil.h +) diff --git a/src/libboardgame_mcts/LastGoodReply.h b/src/libboardgame_mcts/LastGoodReply.h new file mode 100644 index 0000000..25e4485 --- /dev/null +++ b/src/libboardgame_mcts/LastGoodReply.h @@ -0,0 +1,154 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/LastGoodReply.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_LAST_GOOD_REPLY_H +#define LIBBOARDGAME_MCTS_LAST_GOOD_REPLY_H + +#include +#include +#include +#include +#include "Atomic.h" +#include "PlayerMove.h" + +namespace libboardgame_mcts { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Storage for Last-Good-Reply heuristic. + Uses LGRF-2 (Baier, Drake: The Power of Forgetting: Improving the + Last-Good-Reply Policy in Monte-Carlo Go. 2010. + http://webdisk.lclark.edu/drake/publications/baier-drake-ieee-2010.pdf) + To save space, only the player of the reply move is considered when storing + or receiving a reply, the players of the last and second last moves are + ignored. In games without a fixed order of players (i.e. when move + sequences with the same moves but not played by the same players occur), + this can cause undetected collisions. If these collisions are not + sufficiently rare, the last-good-reply heuristic should be disabled in the + search. Undetected collisions can also occur because the replies are stored + in a hash table without collision check. But since the replies have to be + checked for legality in the current position anyway and the collisions are + probably rare, no major negative effect is expected from these collisions. + @tparam M The move type. + @tparam P The (maximum) number of players. + @tparam S The number of entries in the LGR2 has table (per player). + @tparam MT Whether the LGR table is used in a multi-threaded search. */ +template +class LastGoodReply +{ +public: + typedef M Move; + + static const unsigned max_players = P; + + static const size_t hash_table_size = S; + + LastGoodReply(); + + void init(PlayerInt nu_players); + + void store(PlayerInt player, Move last, Move second_last, Move reply); + + void forget(PlayerInt player, Move last, Move second_last, Move reply); + + Move get_lgr1(PlayerInt player, Move last) const; + + Move get_lgr2(PlayerInt player, Move last, Move second_last) const; + +private: + size_t m_hash[Move::range]; + + Atomic m_lgr1[max_players][Move::range]; + + Atomic m_lgr2[max_players][hash_table_size]; + + size_t get_index(Move last, Move second_last) const; +}; + +template +LastGoodReply::LastGoodReply() +{ + mt19937 generator; + for (auto& hash : m_hash) + hash = generator(); +} + +template +inline size_t LastGoodReply::get_index(Move last, + Move second_last) const +{ + size_t hash = (m_hash[last.to_int()] ^ m_hash[second_last.to_int()]); + return hash % hash_table_size; +} + +template +inline auto LastGoodReply::get_lgr1(PlayerInt player, + Move last) const -> Move +{ + return Move(m_lgr1[player][last.to_int()].load(memory_order_relaxed)); +} + +template +inline auto LastGoodReply::get_lgr2( + PlayerInt player, Move last, Move second_last) const -> Move +{ + auto index = get_index(last, second_last); + return Move(m_lgr2[player][index].load(memory_order_relaxed)); +} + +template +void LastGoodReply::init(PlayerInt nu_players) +{ + for (PlayerInt i = 0; i < nu_players; ++i) + if (Move::null().to_int() == 0) + { + // Using memset is ok even if the elements are atomic because + // init() is used before the multi-threaded search starts. + memset(m_lgr1[i], 0, Move::range * sizeof(m_lgr1[i][0])); + memset(m_lgr2[i], 0, hash_table_size * sizeof(m_lgr2[i][0])); + } + else + { + fill(m_lgr1[i], m_lgr1[i] + Move::range, Move::null().to_int()); + fill(m_lgr2[i], m_lgr2[i] + hash_table_size, + Move::null().to_int()); + } +} + +template +inline void LastGoodReply::forget(PlayerInt player, Move last, + Move second_last, Move reply) +{ + auto reply_int = reply.to_int(); + auto null_int = Move::null().to_int(); + { + auto index = get_index(last, second_last); + auto& stored_reply = m_lgr2[player][index]; + if (stored_reply.load(memory_order_relaxed) == reply_int) + stored_reply.store(null_int, memory_order_relaxed); + } + auto& stored_reply = m_lgr1[player][last.to_int()]; + if (stored_reply.load(memory_order_relaxed) == reply_int) + stored_reply.store(null_int, memory_order_relaxed); +} + +template +inline void LastGoodReply::store(PlayerInt player, Move last, + Move second_last, Move reply) +{ + auto reply_int = reply.to_int(); + auto index = get_index(last, second_last); + m_lgr2[player][index].store(reply_int, memory_order_relaxed); + m_lgr1[player][last.to_int()].store(reply_int, memory_order_relaxed); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_LAST_GOOD_REPLY_H diff --git a/src/libboardgame_mcts/Node.h b/src/libboardgame_mcts/Node.h new file mode 100644 index 0000000..5bb8bdd --- /dev/null +++ b/src/libboardgame_mcts/Node.h @@ -0,0 +1,292 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/Node.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_NODE_H +#define LIBBOARDGAME_MCTS_NODE_H + +#include +#include "Atomic.h" +#include "libboardgame_util/Assert.h" + +namespace libboardgame_mcts { + +using namespace std; + +//----------------------------------------------------------------------------- + +typedef uint_least32_t NodeIdx; + +//----------------------------------------------------------------------------- + +/** %Node in a MCTS tree. + For details about how the nodes are used in lock-free multi-threaded mode, + see @ref libboardgame_doc_enz_2009. */ +template +class Node +{ +public: + typedef M Move; + + typedef F Float; + + Node() = default; + + Node(const Node&) = delete; + + Node& operator=(const Node&) = delete; + + /** Initialize the node. + This function may not be called on a node that is already part of + the tree in multi-threaded mode. + The node may be initialized with values and counts greater zero + (prior knowledge) but even if it is initialized with count zero, it + must be initialized with a usable value (e.g. first play urgency for + inner nodes or tie value for the root node). */ + void init(const Move& mv, Float value, Float count); + + /** Initializes the root node. + Does not initialize value and value count as they are not used for the + root. */ + void init_root(); + + const Move& get_move() const; + + /** Number of simulations that went through this node. */ + Float get_visit_count() const; + + /** Number of values that were added. + This count is usually larger than the visit count because in addition + to the terminal values of the simulations, prior knowledge values and + weighted RAVE values could have been added added. */ + Float get_value_count() const; + + /** Value of the node. + For the root node, this is the value of the position from the point of + view of the player at the root node; for all other nodes, this is the + value of the move leading to the position at the node from the point + of view of the player at the parent node. */ + Float get_value() const; + + bool has_children() const; + + unsigned short get_nu_children() const; + + /** Copy the value count from another node without changing the child + information. + This function is not thread-safe and may not be called during the + search. */ + void copy_data_from(const Node& node); + + void link_children(NodeIdx first_child, unsigned short nu_children); + + /** Faster version of link_children() for single-threaded parts of the + code. */ + void link_children_st(NodeIdx first_child, unsigned short nu_children); + + void unlink_children(); + + /** Faster version of unlink_children() for single-threaded parts of the + code. */ + void unlink_children_st(); + + void add_value(Float v, Float weight = 1); + + /** Add a value with weight 1 and remove a previously added loss. + Needed for the implementation of virtual losses in multi-threaded + MCTS and more efficient that a separate add and remove call. */ + void add_value_remove_loss(Float v); + + void inc_visit_count(); + + /** Get node index of first child. + @pre has_children() */ + NodeIdx get_first_child() const; + +private: + Atomic m_value; + + Atomic m_value_count; + + Atomic m_visit_count; + + Atomic m_nu_children; + + Move m_move; + + NodeIdx m_first_child; +}; + +template +void Node::add_value(Float v, Float weight) +{ + // Intentionally uses no synchronization and does not care about + // lost updates in multi-threaded mode + Float count = m_value_count.load(memory_order_relaxed); + Float value = m_value.load(memory_order_relaxed); + count += weight; + value += weight * (v - value) / count; + m_value.store(value, memory_order_relaxed); + m_value_count.store(count, memory_order_relaxed); +} + +template +void Node::add_value_remove_loss(Float v) +{ + // Intentionally uses no synchronization and does not care about + // lost updates in multi-threaded mode + Float count = m_value_count.load(memory_order_relaxed); + if (count == 0) + return; // Adding the virtual loss was a lost update + Float value = m_value.load(memory_order_relaxed); + value += v / count; + m_value.store(value, memory_order_relaxed); +} + +template +void Node::copy_data_from(const Node& node) +{ + // Reminder to update this function when the class gets additional members + struct Dummy + { + Atomic m_value; + Atomic m_value_count; + Atomic m_visit_count; + Atomic m_nu_children; + Move m_move; + NodeIdx m_first_child; + }; + static_assert(sizeof(Node) == sizeof(Dummy), ""); + + m_move = node.m_move; + // Load/store relaxed (it wouldn't even need to be atomic) because this + // function is only used before the multi-threaded search. + m_value_count.store(node.m_value_count.load(memory_order_relaxed), + memory_order_relaxed); + m_value.store(node.m_value.load(memory_order_relaxed), + memory_order_relaxed); + m_visit_count.store(node.m_visit_count.load(memory_order_relaxed), + memory_order_relaxed); +} + +template +inline auto Node::get_value_count() const -> Float +{ + return m_value_count.load(memory_order_relaxed); +} + +template +inline NodeIdx Node::get_first_child() const +{ + LIBBOARDGAME_ASSERT(has_children()); + return m_first_child; +} + +template +inline auto Node::get_move() const -> const Move& +{ + return m_move; +} + +template +inline unsigned short Node::get_nu_children() const +{ + return m_nu_children.load(memory_order_acquire); +} + +template +inline auto Node::get_value() const -> Float +{ + return m_value.load(memory_order_relaxed); +} + +template +inline auto Node::get_visit_count() const -> Float +{ + return m_visit_count.load(memory_order_relaxed); +} + +template +inline bool Node::has_children() const +{ + return get_nu_children() > 0; +} + +template +inline void Node::inc_visit_count() +{ + // We don't care about the unlikely case that updates are lost because + // incrementing is not atomic + Float count = m_visit_count.load(memory_order_relaxed); + ++count; + m_visit_count.store(count, memory_order_relaxed); +} + +template +void Node::init(const Move& mv, Float value, Float count) +{ + // The node is not yet visible to other threads because init() is called + // before the children are linked to its parent with link_children() + // (which does a memory_order_release on m_nu_children of the parent). + // Therefore, the most efficient way here is to initialize all values with + // memory_order_relaxed. + m_move = mv; + m_value_count.store(count, memory_order_relaxed); + m_value.store(value, memory_order_relaxed); + m_visit_count.store(0, memory_order_relaxed); + m_nu_children.store(0, memory_order_relaxed); +} + +template +void Node::init_root() +{ +#if LIBBOARDGAME_DEBUG + m_move = Move::null(); +#endif + m_visit_count.store(0, memory_order_relaxed); + m_nu_children.store(0, memory_order_relaxed); +} + +template +inline void Node::link_children(NodeIdx first_child, + unsigned short nu_children) +{ + LIBBOARDGAME_ASSERT(nu_children < Move::range); + // first_child cannot be 0 because 0 is always used for the root node + LIBBOARDGAME_ASSERT(first_child != 0); + m_first_child = first_child; + m_nu_children.store(nu_children, memory_order_release); +} + +template +inline void Node::link_children_st(NodeIdx first_child, + unsigned short nu_children) +{ + LIBBOARDGAME_ASSERT(nu_children < Move::range); + // first_child cannot be 0 because 0 is always used for the root node + LIBBOARDGAME_ASSERT(first_child != 0); + m_first_child = first_child; + // Store relaxed (wouldn't even need to be atomic) + m_nu_children.store(nu_children, memory_order_relaxed); +} + +template +inline void Node::unlink_children() +{ + m_nu_children.store(0, memory_order_release); +} + +template +inline void Node::unlink_children_st() +{ + // Store relaxed (wouldn't even need to be atomic) + m_nu_children.store(0, memory_order_relaxed); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_NODE_H diff --git a/src/libboardgame_mcts/PlayerMove.h b/src/libboardgame_mcts/PlayerMove.h new file mode 100644 index 0000000..1bf6b9a --- /dev/null +++ b/src/libboardgame_mcts/PlayerMove.h @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/PlayerMove.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_PLAYER_MOVE_H +#define LIBBOARDGAME_MCTS_PLAYER_MOVE_H + +#include + +namespace libboardgame_mcts { + +//----------------------------------------------------------------------------- + +typedef uint_fast8_t PlayerInt; + +//----------------------------------------------------------------------------- + +template +struct PlayerMove +{ + PlayerInt player; + + MOVE move; + + PlayerMove() = default; + + PlayerMove(PlayerInt player, MOVE move) + { + this->player = player; + this->move = move; + } +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_PLAYER_MOVE_H diff --git a/src/libboardgame_mcts/SearchBase.h b/src/libboardgame_mcts/SearchBase.h new file mode 100644 index 0000000..1a99927 --- /dev/null +++ b/src/libboardgame_mcts/SearchBase.h @@ -0,0 +1,1544 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/SearchBase.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_SEARCH_BASE_H +#define LIBBOARDGAME_MCTS_SEARCH_BASE_H + +#include +#include +#include +#include +#include +#include "Atomic.h" +#include "LastGoodReply.h" +#include "PlayerMove.h" +#include "Tree.h" +#include "TreeUtil.h" +#include "libboardgame_util/Abort.h" +#include "libboardgame_util/ArrayList.h" +#include "libboardgame_util/Barrier.h" +#include "libboardgame_util/IntervalChecker.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/MathUtil.h" +#include "libboardgame_util/Statistics.h" +#include "libboardgame_util/StringUtil.h" +#include "libboardgame_util/TimeIntervalChecker.h" +#include "libboardgame_util/Timer.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_mcts { + +using namespace std; +using libboardgame_mcts::tree_util::find_node; +using libboardgame_util::get_abort; +using libboardgame_util::time_to_string; +using libboardgame_util::to_string; +using libboardgame_util::ArrayList; +using libboardgame_util::Barrier; +using libboardgame_util::IntervalChecker; +using libboardgame_util::RandomGenerator; +using libboardgame_util::StatisticsBase; +using libboardgame_util::StatisticsDirtyLockFree; +using libboardgame_util::StatisticsExt; +using libboardgame_util::Timer; +using libboardgame_util::TimeIntervalChecker; +using libboardgame_util::TimeSource; + +//----------------------------------------------------------------------------- + +#define LIBBOARDGAME_LOG_THREAD(thread_state, ...) \ + LIBBOARDGAME_LOG('[', thread_state.thread_id, "] ", __VA_ARGS__) + +//----------------------------------------------------------------------------- + +/** Default optional compile-time parameters for Search. + See description of class Search for more information. */ +struct SearchParamConstDefault +{ + /** The floating type used for mean values and counts. + The default type is @c float for a reduced node size and performance + gains (especially on 32-bit systems). However, using @c float sets a + practical limit on the number of simulations before the count and mean + values go into saturation. This maximum is given by 2^d-1 with d being + the digits in the mantissa (=23 for IEEE 754 float's). The search will + terminate when this number is reached. For longer searches, the code + should be compiled with floating type @c double. */ + typedef float Float; + + /** The maximum number of players. */ + static const PlayerInt max_players = 2; + + /** The maximum length of a game. */ + static const unsigned max_moves = 1000; + + /** Compile with support for multi-threaded search. + Disabling this slightly increases the performance if support for a + multi-threaded search is not needed. */ + static const bool multithread = true; + + /** Use RAVE. */ + static const bool rave = false; + + /** Enable distance weighting of RAVE updates. + The weight decreases linearly from the start to the end of a + simulation. The distance weight is applied in addition to the normal + RAVE weight. */ + static const bool rave_dist_weighting = false; + + /** Enable Last-Good-Reply heuristic. + @see LastGoodReply */ + static const bool use_lgr = false; + + /** See LastGoodReply::hash_table_size. + Must be greater 0 if use_lgr is true. */ + static const size_t lgr_hash_table_size = 0; + + /** Use virtual loss in multi-threaded mode. + See Chaslot et al.: Parallel Monte-Carlo Tree Search. 2008. */ + static const bool virtual_loss = false; + + /** Terminate search early if move is unlikely to change. + See implementation of check_cannot_change(). */ + static const bool use_unlikely_change = true; + + /** The minimum count used in prior knowledge initialization of + the children of an expanded node. + The value must be greater 0 (it may be a positive epsilon) because + otherwise the search would need to handle a special case in the bias + term computation. */ + static constexpr Float child_min_count = 0; + + /** An evaluation value representing a 50% winning probability. */ + static constexpr Float tie_value = 0.5f; + + /** Value to start the tree pruning with. + This value should be above typical count initializations if prior + knowledge initialization is used. */ + static constexpr Float prune_count_start = 16; + + /** Expected simulations per second. + If the simulations per second vary a lot, it should be a value closer + to the lower values. This value is used, for example, to determine an + interval for checking expensive abort conditions in deterministic mode + (in regular mode, the simulations per second will be measured and the + interval will be adjusted automatically). That means that in + deterministic mode, a pessimistic low value will cause more calls to + the expensive function but an optimistic high value will delay aborting + the search. */ + static constexpr double expected_sim_per_sec = 100; +}; + +//----------------------------------------------------------------------------- + +/** Game-independent Monte-Carlo tree search. + Game-dependent functionality is added by implementing some pure virtual + functions and by template parameters. + + RAVE (see @ref libboardgame_doc_rave) is implemented differently from + the algorithm described in the original paper: RAVE values are not stored + separately in the nodes but added to the normal values with a certain + (constant) weight and up to a maximum visit count of the parent node. This + saves memory in the tree and speeds up move selection in the in-tree phase. + It is weaker than the original RAVE at a low number of simulations but + seems to be equally good or even better at a high number of simulations. + + @tparam S The game-dependent state of a simulation. The state provides + functions for move generation, evaluation of terminal positions, etc. The + state should be thread-safe to support multiple states if multi-threading + is used. + @tparam M The move type. The type must be convertible to an integer by + providing M::to_int() and M::range. + @tparam R Optional compile-time parameters, see SearchParamConstDefault */ +template +class SearchBase +{ +public: + typedef S State; + + typedef M Move; + + typedef R SearchParamConst; + + static const bool multithread = SearchParamConst::multithread; + + typedef typename SearchParamConst::Float Float; + + typedef libboardgame_mcts::Node Node; + + typedef libboardgame_mcts::Tree Tree; + + typedef libboardgame_mcts::PlayerMove PlayerMove; + + static const PlayerInt max_players = SearchParamConst::max_players; + + static const unsigned max_moves = SearchParamConst::max_moves; + + static const size_t lgr_hash_table_size = + SearchParamConst::lgr_hash_table_size; + + static_assert(! SearchParamConst::use_lgr || lgr_hash_table_size > 0, ""); + + + /** Constructor. + @param nu_threads + @param memory The memory to be used for (all) the search trees. */ + SearchBase(unsigned nu_threads, size_t memory); + + virtual ~SearchBase(); + + + /** @name Pure virtual functions */ + /** @{ */ + + /** Create a new game-specific state to be used in a thread of the + search. */ + virtual unique_ptr create_state() = 0; + + /** Get the current number of players. */ + virtual PlayerInt get_nu_players() const = 0; + + /** Get player to play at root node of the search. */ + virtual PlayerInt get_player() const = 0; + + /** @} */ // @name + + + /** @name Virtual functions */ + /** @{ */ + + /** Check if the position at the root is a follow-up position of the last + search. + In this function, the subclass can store the game state at the root of + the search, compare it to the the one of the last search, check if + the current state is a follow-up position and return the move sequence + leading from the last position to the current one, so that the search + can check if a subtree of the last search can be reused. + This function will be called exactly once at the beginning of each + search. The default implementation returns false. + The information is also used for deciding whether to clear other + caches from the last search (e.g. Last-Good-Reply heuristic). */ + virtual bool check_followup(ArrayList& sequence); + + virtual string get_info() const; + + virtual string get_info_ext() const; + + /** @} */ // @name + + + /** @name Parameters */ + /** @{ */ + + /** Minimum count of a node to be expanded. */ + void set_expand_threshold(Float n); + + Float get_expand_threshold() const; + + /** Increase of the expand threshold per in-tree move played. */ + void set_expand_threshold_inc(Float n); + + Float get_expand_threshold_inc() const; + + /** Constant used in the exploration term. + The exploration term has the form c * sqrt(parent_count) / child_count + with a configurable constant c. It assumes that children counts are + initialized greater than 0. */ + void set_exploration_constant(Float c) { m_exploration_constant = c; } + + Float get_exploration_constant() const { return m_exploration_constant; } + + /** Reuse the subtree from the previous search if the current position is + a follow-up position of the previous one. */ + void set_reuse_subtree(bool enable); + + bool get_reuse_subtree() const; + + /** Reuse the tree from the previous search if the current position is + the same position as the previous one. */ + void set_reuse_tree(bool enable); + + bool get_reuse_tree() const; + + /** Maximum parent visit count for applying RAVE. */ + void set_rave_parent_max(Float value); + + Float get_rave_parent_max() const; + + /** Maximum child value count for applying RAVE. */ + void set_rave_child_max(Float value); + + Float get_rave_child_max() const; + + /** Weight used for adding RAVE values to the node value. */ + void set_rave_weight(Float value); + + Float get_rave_weight() const; + + /** @} */ // @name + + + /** Run a search. + @param[out] mv + @param max_count Number of simulations to run. The search might return + earlier if the best move cannot change anymore or if the count of the + root node was initialized from an init tree + @param min_simulations + @param max_time Maximum search time. Only used if max_count is zero + @param time_source Time source for time measurement + @return @c false if no move could be generated because the position is + a terminal position. */ + bool search(Move& mv, Float max_count, size_t min_simulations, + double max_time, TimeSource& time_source); + + const Tree& get_tree() const; + +#if LIBBOARDGAME_DEBUG + string dump() const; +#endif + + /** Number of simulations in the current search in all threads. */ + size_t get_nu_simulations() const; + + /** Select the move to play. + Uses select_final(). */ + bool select_move(Move& mv) const; + + /** Select the best child of the root node after the search. + Selects child with highest number of wins; the value is used as a + tie-breaker for equal counts (important at very low number of + simulations, e.g. all children have count 1 or 0). */ + const Node* select_final() const; + + State& get_state(unsigned thread_id); + + const State& get_state(unsigned thread_id) const; + + /** Set a callback function that informs the caller about the + estimated time left. + The callback function will be called about every 0.1s. The arguments + of the callback function are: elapsed time, estimated remaining time. */ + void set_callback(function callback); + + /** Get evaluation for a player at root node. */ + const StatisticsDirtyLockFree& get_root_val(PlayerInt player) const; + + /** Get evaluation for get_player() at root node. */ + const StatisticsDirtyLockFree& get_root_val() const; + + /** The number of times the root node was visited. + This is equal to the number of simulations plus the visit count + of a subtree reused from the previous search. */ + Float get_root_visit_count() const; + + /** Create the threads used in the search. + This cannot be done in the constructor because it uses the virtual + function create_state(). This function will automatically be called + before a search if the threads have not been constructed yet, but it + is advisable to explicitely call it in the constructor of the subclass + to save some time at the first move generation where the game clock + might already be running. */ + void create_threads(); + +protected: + struct Simulation + { + ArrayList nodes; + + ArrayList moves; + + array eval; + }; + + virtual void on_start_search(bool is_followup); + + /** Time source for current search. + Only valid during a search. */ + TimeSource& get_time_source(); + +private: +#if LIBBOARDGAME_DEBUG + class AssertionHandler + : public libboardgame_util::AssertionHandler + { + public: + AssertionHandler(const SearchBase& search); + + ~AssertionHandler(); + + void run() override; + + private: + const SearchBase& m_search; + }; +#endif + + /** Thread-specific search state. */ + struct ThreadState + { + unique_ptr state; + + unsigned thread_id; + + /** Was the search in this thread terminated because the search tree + was full? */ + bool is_out_of_mem; + + Simulation simulation; + + StatisticsExt<> stat_len; + + StatisticsExt<> stat_in_tree_len; + + /** Local variable for update_rave(). + Reused for efficiency. */ + array was_played; + + /** Local variable for update_rave(). + Reused for efficiency. */ + array first_play; + + ~ThreadState(); + }; + + /** Thread in the parallel search. + The thread waits for a call to start_search(), then runs + SearchBase::search_loop()) with the thread-specific search state. + After start_search(), wait_search_finished() needs to called before + calling start_search() again or destructing this object. */ + class Thread + { + public: + typedef function SearchFunc; + + ThreadState thread_state; + + explicit Thread(SearchFunc& search_func); + + ~Thread(); + + void run(); + + void start_search(); + + void wait_search_finished(); + + private: + SearchFunc m_search_func; + + bool m_quit = false; + + bool m_start_search_flag = false; + + bool m_search_finished_flag = false; + + Barrier m_thread_ready{2}; + + mutex m_start_search_mutex; + + mutex m_search_finished_mutex; + + condition_variable m_start_search_cond; + + condition_variable m_search_finished_cond; + + unique_lock m_search_finished_lock{m_search_finished_mutex, + defer_lock}; + + thread m_thread; + + void thread_main(); + }; + + + /** @name Members that are used concurrently by all threads during the + lock-free multi-threaded search */ + /** @{ */ + + Tree m_tree; + + /** See get_root_val(). */ + array, max_players> m_root_val; + + LastGoodReply m_lgr; + + /** See get_nu_simulations(). */ + Atomic m_nu_simulations; + + /** @} */ // @name + + + unsigned m_nu_threads; + + Float m_expand_threshold = 0; + + Float m_expand_threshold_inc = 0; + + bool m_deterministic; + + bool m_reuse_subtree = true; + + bool m_reuse_tree = false; + + /** Player to play at the root node of the search. */ + PlayerInt m_player; + + /** Cached return value of get_nu_players() that stays constant during + a search. */ + PlayerInt m_nu_players; + + /** Time of last search. */ + double m_last_time; + + Float m_rave_parent_max = 50000; + + Float m_rave_child_max = 2000; + + Float m_rave_weight = 0.3f; + + /** Minimum simulations to perform in the current search. + This does not include the count of simulations reused from a subtree of + a previous search. */ + size_t m_min_simulations; + + /** Maximum simulations of current search. + This include the count of simulations reused from a subtree of a + previous search. */ + Float m_max_count; + + /** Maximum time of current search. */ + double m_max_time; + + TimeSource* m_time_source; + + Float m_exploration_constant; + + Timer m_timer; + + vector> m_threads; + + Tree m_tmp_tree; + +#if LIBBOARDGAME_DEBUG + AssertionHandler m_assertion_handler; +#endif + + + function m_callback; + + ArrayList m_followup_sequence; + + bool check_abort(const ThreadState& thread_state) const; + + LIBBOARDGAME_NOINLINE + bool check_abort_expensive(ThreadState& thread_state) const; + + bool check_cannot_change(ThreadState& thread_state, Float remaining) const; + + bool estimate_reused_root_val(Tree& tree, const Node& root, Float& value, + Float& count); + + bool expand_node(ThreadState& thread_state, const Node& node, + const Node*& best_child); + + void playout(ThreadState& thread_state); + + void play_in_tree(ThreadState& thread_state); + + bool prune(TimeSource& time_source, double time, Float prune_min_count, + Float& new_prune_min_count); + + void search_loop(ThreadState& thread_state); + + const Node* select_child(const Node& node); + + void update_lgr(ThreadState& thread_state); + + void update_rave(ThreadState& thread_state); + + void update_values(ThreadState& thread_state); +}; + + +template +SearchBase::ThreadState::~ThreadState() = default; + +template +SearchBase::Thread::Thread(SearchFunc& search_func) + : m_search_func(search_func) +{ } + +template +SearchBase::Thread::~Thread() +{ + if (! m_thread.joinable()) + return; + m_quit = true; + { + lock_guard lock(m_start_search_mutex); + m_start_search_flag = true; + } + m_start_search_cond.notify_one(); + m_thread.join(); +} + +template +void SearchBase::Thread::run() +{ + m_thread = thread(bind(&Thread::thread_main, this)); + m_thread_ready.wait(); +} + +template +void SearchBase::Thread::start_search() +{ + LIBBOARDGAME_ASSERT(m_thread.joinable()); + m_search_finished_lock.lock(); + { + lock_guard lock(m_start_search_mutex); + m_start_search_flag = true; + } + m_start_search_cond.notify_one(); +} + +template +void SearchBase::Thread::thread_main() +{ + unique_lock lock(m_start_search_mutex); + m_thread_ready.wait(); + while (true) + { + while (! m_start_search_flag) + m_start_search_cond.wait(lock); + m_start_search_flag = false; + if (m_quit) + break; + m_search_func(thread_state); + { + lock_guard lock(m_search_finished_mutex); + m_search_finished_flag = true; + } + m_search_finished_cond.notify_one(); + } +} + +template +void SearchBase::Thread::wait_search_finished() +{ + LIBBOARDGAME_ASSERT(m_thread.joinable()); + while (! m_search_finished_flag) + m_search_finished_cond.wait(m_search_finished_lock); + m_search_finished_flag = false; + m_search_finished_lock.unlock(); +} + + +#if LIBBOARDGAME_DEBUG +template +SearchBase::AssertionHandler::AssertionHandler( + const SearchBase& search) + : m_search(search) +{ +} + +template +SearchBase::AssertionHandler::~AssertionHandler() = default; + +template +void SearchBase::AssertionHandler::run() +{ + LIBBOARDGAME_LOG(m_search.dump()); +} +#endif // LIBBOARDGAME_DEBUG + + +template +SearchBase::SearchBase(unsigned nu_threads, size_t memory) + : m_tree(memory / 2, nu_threads), + m_nu_threads(nu_threads), + m_exploration_constant(0), + m_tmp_tree(memory / 2, m_nu_threads) +#if LIBBOARDGAME_DEBUG + , m_assertion_handler(*this) +#endif +{ } + +template +SearchBase::~SearchBase() = default; + +template +bool SearchBase::check_abort(const ThreadState& thread_state) const +{ +#if LIBBOARDGAME_DISABLE_LOG + LIBBOARDGAME_UNUSED(thread_state); +#endif + if (m_max_count > 0 && m_tree.get_root().get_visit_count() >= m_max_count) + { + LIBBOARDGAME_LOG_THREAD(thread_state, "Maximum count reached"); + return true; + } + return false; +} + +template +bool SearchBase::check_abort_expensive( + ThreadState& thread_state) const +{ + if (get_abort()) + { + LIBBOARDGAME_LOG_THREAD(thread_state, "Search aborted"); + return true; + } + static_assert(numeric_limits::radix == 2, ""); + auto count = m_tree.get_root().get_visit_count(); + if (count >= (size_t(1) << numeric_limits::digits) - 1) + { + LIBBOARDGAME_LOG_THREAD(thread_state, + "Max count supported by float exceeded"); + return true; + } + auto time = m_timer(); + if (! m_deterministic && time < 0.1) + // Simulations per second might be inaccurate for very small times + return false; + double simulations_per_sec; + if (time == 0) + simulations_per_sec = SearchParamConst::expected_sim_per_sec; + else + { + size_t nu_simulations = m_nu_simulations.load(memory_order_relaxed); + simulations_per_sec = double(nu_simulations) / time; + } + double remaining_time; + Float remaining_simulations; + if (m_max_count == 0) + { + // Search uses time limit + if (time > m_max_time) + { + LIBBOARDGAME_LOG_THREAD(thread_state, "Maximum time reached"); + return true; + } + remaining_time = m_max_time - time; + remaining_simulations = Float(remaining_time * simulations_per_sec); + } + else + { + // Search uses count limit + remaining_simulations = m_max_count - count; + remaining_time = remaining_simulations / simulations_per_sec; + } + if (thread_state.thread_id == 0 && m_callback) + m_callback(time, remaining_time); + if (check_cannot_change(thread_state, remaining_simulations)) + return true; + return false; +} + +template +bool SearchBase::check_cannot_change(ThreadState& thread_state, + Float remaining) const +{ +#if LIBBOARDGAME_DISABLE_LOG + LIBBOARDGAME_UNUSED(thread_state); +#endif + // select_final() selects move with highest number of wins. + Float max_wins = 0; + Float second_max = 0; + for (auto& i : m_tree.get_root_children()) + { + Float wins = i.get_value() * i.get_value_count(); + if (wins > max_wins) + { + second_max = max_wins; + max_wins = wins; + } + } + Float diff = max_wins - second_max; + if (SearchParamConst::use_unlikely_change) + { + // Weight remaining number of simulations with current global win rate, + // but not less than 10% + auto& root_val = m_root_val[m_player]; + Float win_rate; + if (root_val.get_count() > 100) + { + win_rate = root_val.get_mean(); + if (win_rate < 0.1f) + win_rate = 0.1f; + } + else + win_rate = 1; // Not enough statistics + if (diff < win_rate * remaining) + return false; + } + else if (diff < remaining) + return false; + LIBBOARDGAME_LOG_THREAD(thread_state, "Move will not change"); + return true; +} + +template +bool SearchBase::check_followup(ArrayList& sequence) +{ + LIBBOARDGAME_UNUSED(sequence); + return false; +} + +template +void SearchBase::create_threads() +{ + if (! multithread && m_nu_threads > 1) + throw runtime_error("libboardgame_mcts::Search was compiled" + " without support for multithreading"); + LIBBOARDGAME_LOG("Creating ", m_nu_threads, " threads"); + m_threads.clear(); + m_threads.reserve(m_nu_threads); + auto search_func = + static_cast( + bind(&SearchBase::search_loop, this, placeholders::_1)); + for (unsigned i = 0; i < m_nu_threads; ++i) + { + unique_ptr t(new Thread(search_func)); + auto& thread_state = t->thread_state; + thread_state.thread_id = i; + thread_state.state = create_state(); + for (auto& was_played : thread_state.was_played) + was_played = max_players; + if (i > 0) + t->run(); + m_threads.push_back(move(t)); + } +} + +#if LIBBOARDGAME_DEBUG +template +string SearchBase::dump() const +{ + ostringstream s; + for (unsigned i = 0; i < m_nu_threads; ++i) + { + s << "Thread state " << i << ":\n" + << get_state(i).dump(); + } + return s.str(); +} +#endif + +template +bool SearchBase::expand_node(ThreadState& thread_state, + const Node& node, + const Node*& best_child) +{ + auto& state = *thread_state.state; + auto thread_id = thread_state.thread_id; + typename Tree::NodeExpander expander(thread_id, m_tree, + SearchParamConst::child_min_count); + auto root_val = m_root_val[state.get_player()].get_mean(); + if (state.gen_children(expander, root_val)) + { + expander.link_children(m_tree, node); + best_child = expander.get_best_child(); + return true; + } + return false; +} + +template +inline auto SearchBase::get_expand_threshold() const -> Float +{ + return m_expand_threshold; +} + +template +inline auto SearchBase::get_expand_threshold_inc() const -> Float +{ + return m_expand_threshold_inc; +} + +template +inline size_t SearchBase::get_nu_simulations() const +{ + return m_nu_simulations; +} + +template +inline auto SearchBase::get_root_val(PlayerInt player) const +-> const StatisticsDirtyLockFree& +{ + LIBBOARDGAME_ASSERT(player < m_nu_players); + return m_root_val[player]; +} + +template +inline auto SearchBase::get_root_val() const +-> const StatisticsDirtyLockFree& +{ + return get_root_val(get_player()); +} + +template +inline auto SearchBase::get_root_visit_count() const -> Float +{ + return m_tree.get_root().get_visit_count(); +} + +template +inline auto SearchBase::get_rave_parent_max() const -> Float +{ + return m_rave_parent_max; +} + +template +inline auto SearchBase::get_rave_child_max() const -> Float +{ + return m_rave_child_max; +} + +template +inline auto SearchBase::get_rave_weight() const -> Float +{ + return m_rave_weight; +} + +template +inline bool SearchBase::get_reuse_subtree() const +{ + return m_reuse_subtree; +} + +template +inline bool SearchBase::get_reuse_tree() const +{ + return m_reuse_tree; +} + +template +inline S& SearchBase::get_state(unsigned thread_id) +{ + LIBBOARDGAME_ASSERT(thread_id < m_threads.size()); + return *m_threads[thread_id]->thread_state.state; +} + +template +inline const S& SearchBase::get_state(unsigned thread_id) const +{ + LIBBOARDGAME_ASSERT(thread_id < m_threads.size()); + return *m_threads[thread_id]->thread_state.state; +} + +template +inline TimeSource& SearchBase::get_time_source() +{ + LIBBOARDGAME_ASSERT(m_time_source != 0); + return *m_time_source; +} + +template +inline auto SearchBase::get_tree() const -> const Tree& +{ + return m_tree; +} + +template +void SearchBase::on_start_search(bool is_followup) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(is_followup); +} + +template +void SearchBase::playout(ThreadState& thread_state) +{ + auto& state = *thread_state.state; + state.start_playout(); + auto& simulation = thread_state.simulation; + auto& moves = simulation.moves; + auto nu_moves = moves.size(); + Move last = nu_moves > 0 ? moves[nu_moves - 1].move : Move::null(); + Move second_last = nu_moves > 1 ? moves[nu_moves - 2].move : Move::null(); + PlayerMove mv; + while (state.gen_playout_move(m_lgr, last, second_last, mv)) + { + state.play_playout(mv.move); + moves.push_back(mv); + second_last = last; + last = mv.move; + } +} + +template +void SearchBase::play_in_tree(ThreadState& thread_state) +{ + auto& state = *thread_state.state; + auto& simulation = thread_state.simulation; + simulation.nodes.resize(1); + simulation.moves.clear(); + auto& root = m_tree.get_root(); + auto node = &root; + Float expand_threshold = m_expand_threshold; + while (node->has_children()) + { + node = select_child(*node); + if (multithread && SearchParamConst::virtual_loss) + m_tree.add_value(*node, 0); + simulation.nodes.push_back(node); + Move mv = node->get_move(); + simulation.moves.push_back(PlayerMove(state.get_player(), mv)); + state.play_in_tree(mv); + expand_threshold += m_expand_threshold_inc; + } + state.finish_in_tree(); + if (node->get_visit_count() > expand_threshold) + { + if (! expand_node(thread_state, *node, node)) + thread_state.is_out_of_mem = true; + else if (node) + { + simulation.nodes.push_back(node); + Move mv = node->get_move(); + simulation.moves.push_back(PlayerMove(state.get_player(), mv)); + state.play_expanded_child(mv); + } + } + thread_state.stat_in_tree_len.add(double(simulation.moves.size())); +} + +template +string SearchBase::get_info() const +{ + auto& root = m_tree.get_root(); + if (m_threads.empty()) + return string(); + auto& thread_state = m_threads[0]->thread_state; + ostringstream s; + s << fixed << setprecision(2) << "Val: " << get_root_val().get_mean() + << setprecision(0) << ", ValCnt: " << get_root_val().get_count() + << ", VstCnt: " << get_root_visit_count() + << ", Sim: " << m_nu_simulations; + auto child = select_final(); + if (child && root.get_visit_count() > 0) + s << setprecision(1) << ", Chld: " + << (100 * child->get_visit_count() / root.get_visit_count()) + << '%'; + s << "\nNds: " << m_tree.get_nu_nodes() + << ", Tm: " << time_to_string(m_last_time) + << setprecision(0) << ", Sim/s: " + << (double(m_nu_simulations) / m_last_time) + << ", Len: " << thread_state.stat_len.to_string(true, 1, true) + << "\nDp: " << thread_state.stat_in_tree_len.to_string(true, 1, true) + << "\n"; + return s.str(); +} + +template +string SearchBase::get_info_ext() const +{ + return string(); +} + +template +bool SearchBase::prune(TimeSource& time_source, double time, + Float prune_min_count, + Float& new_prune_min_count) +{ +#if LIBBOARDGAME_DISABLE_LOG + LIBBOARDGAME_UNUSED(time); +#endif + Timer timer(time_source); + m_tmp_tree.clear(); + m_tree.copy_subtree(m_tmp_tree, m_tmp_tree.get_root(), m_tree.get_root(), + prune_min_count); + int percent = int(m_tmp_tree.get_nu_nodes() * 100 / m_tree.get_nu_nodes()); + LIBBOARDGAME_LOG("Pruning MinCnt: ", prune_min_count, ", AtTm: ", time, + ", Nds: ", m_tmp_tree.get_nu_nodes(), " (", percent, + "%), Tm: ", timer()); + m_tree.swap(m_tmp_tree); + if (percent > 50) + { + if (prune_min_count >= 0.5 * numeric_limits::max()) + return false; + new_prune_min_count = prune_min_count * 2; + return true; + } + else + { + new_prune_min_count = prune_min_count; + return true; + } +} + +/** Estimate the value and count of a root node from its children. + After reusing a subtree, we don't know the value of the root because nodes + only store the value of moves. To estimate the root value, we use the child + with the highest visit count. */ +template +bool SearchBase::estimate_reused_root_val(Tree& tree, + const Node& root, + Float& value, Float& count) +{ + const Node* best = nullptr; + Float max_count = 0; + for (auto& i : tree.get_children(root)) + if (i.get_visit_count() > max_count) + { + best = &i; + max_count = i.get_visit_count(); + } + if (! best) + return false; + value = best->get_value(); + count = best->get_value_count(); + return count > 0; +} + +template +bool SearchBase::search(Move& mv, Float max_count, + size_t min_simulations, double max_time, + TimeSource& time_source) +{ + if (m_nu_threads != m_threads.size()) + create_threads(); + m_deterministic = RandomGenerator::has_global_seed(); + bool is_followup = check_followup(m_followup_sequence); + on_start_search(is_followup); + if (max_count > 0) + // A fixed number of simulations means that no time limit is used, but + // max_time is still used at some places in the code, so we set it to + // infinity + max_time = numeric_limits::max(); + m_player = get_player(); + m_nu_players = get_nu_players(); + bool clear_tree = true; + bool is_same = false; + if (is_followup && m_followup_sequence.empty()) + { + is_same = true; + is_followup = false; + } + if (is_same || (is_followup && m_followup_sequence.size() <= m_nu_players)) + { + // Use root_val from last search but with a count of max. 100 + for (PlayerInt i = 0; i < m_nu_players; ++i) + if (m_root_val[i].get_count() > 100) + m_root_val[i].init(m_root_val[i].get_mean(), 100); + } + else + for (PlayerInt i = 0; i < m_nu_players; ++i) + m_root_val[i].init(SearchParamConst::tie_value, 1); + if ((m_reuse_subtree && is_followup) || (m_reuse_tree && is_same)) + { + size_t tree_nodes = m_tree.get_nu_nodes(); + if (m_followup_sequence.empty()) + { + if (tree_nodes > 1) + LIBBOARDGAME_LOG("Reusing all ", tree_nodes, "nodes (count=", + m_tree.get_root().get_visit_count(), ")"); + } + else + { + Timer timer(time_source); + m_tmp_tree.clear(); + auto node = find_node(m_tree, m_followup_sequence); + if (node) + { + m_tree.extract_subtree(m_tmp_tree, *node); + auto& tmp_tree_root = m_tmp_tree.get_root(); + if (! is_same) + { + Float value, count; + if (estimate_reused_root_val(m_tmp_tree, tmp_tree_root, + value, count)) + m_root_val[m_player].add(value, count); + } + size_t tmp_tree_nodes = m_tmp_tree.get_nu_nodes(); + if (tree_nodes > 1 && tmp_tree_nodes > 1) + { + double time = timer(); + LIBBOARDGAME_LOG("Reusing ", tmp_tree_nodes, " nodes (", + std::fixed, setprecision(1), + 100 * double(tmp_tree_nodes) + / double(tree_nodes), + "% tm=", setprecision(4), time, ")"); + m_tree.swap(m_tmp_tree); + clear_tree = false; + max_time -= time; + if (max_time < 0) + max_time = 0; + } + } + } + } + if (clear_tree) + m_tree.clear(); + + m_timer.reset(time_source); + m_time_source = &time_source; + if (SearchParamConst::use_lgr && ! is_followup) + m_lgr.init(m_nu_players); + for (auto& i : m_threads) + { + auto& thread_state = i->thread_state; + thread_state.stat_len.clear(); + thread_state.stat_in_tree_len.clear(); + thread_state.state->start_search(); + } + m_max_count = max_count; + m_min_simulations = min_simulations; + m_max_time = max_time; + m_nu_simulations.store(0); + Float prune_min_count = SearchParamConst::prune_count_start; + + // Don't use multi-threading for very short searches (less than 0.5s). + auto reused_count = m_tree.get_root().get_visit_count(); + unsigned nu_threads = m_nu_threads; + double expected_time; + if (max_count > 0) + expected_time = + (max_count - reused_count) + / SearchParamConst::expected_sim_per_sec; + else + expected_time = max_time; + if (nu_threads > 1 && expected_time < 0.5) + { + LIBBOARDGAME_LOG("Using single-threading for short search"); + nu_threads = 1; + } + + auto& thread_state_0 = m_threads[0]->thread_state; + auto& root = m_tree.get_root(); + if (! root.has_children()) + { + const Node* best_child; + thread_state_0.state->start_simulation(0); + thread_state_0.state->finish_in_tree(); + expand_node(thread_state_0, root, best_child); + } + + if (root.get_nu_children() == 0) + LIBBOARDGAME_LOG("No legal moves at root"); + else if (root.get_nu_children() == 1 && min_simulations == 0) + LIBBOARDGAME_LOG("Root has only one child"); + else + while (true) + { + for (unsigned i = 1; i < nu_threads; ++i) + m_threads[i]->start_search(); + search_loop(thread_state_0); + for (unsigned i = 1; i < nu_threads; ++i) + m_threads[i]->wait_search_finished(); + bool is_out_of_mem = false; + for (unsigned i = 0; i < nu_threads; ++i) + if (m_threads[i]->thread_state.is_out_of_mem) + { + is_out_of_mem = true; + break; + } + if (! is_out_of_mem) + break; + double time = m_timer(); + prune(time_source, time, prune_min_count, prune_min_count); + } + + m_last_time = m_timer(); + LIBBOARDGAME_LOG(get_info()); + bool result = select_move(mv); + m_time_source = nullptr; + return result; +} + +template +void SearchBase::search_loop(ThreadState& thread_state) +{ + auto& state = *thread_state.state; + auto& simulation = thread_state.simulation; + simulation.nodes.assign(&m_tree.get_root()); + simulation.moves.clear(); + double time_interval = 0.1; + if (m_max_count == 0 && m_max_time < 1) + time_interval = 0.1 * m_max_time; + IntervalChecker expensive_abort_checker( + *m_time_source, time_interval, + bind(&SearchBase::check_abort_expensive, this, + ref(thread_state))); + if (m_deterministic) + { + unsigned interval = + static_cast( + max(1.0, SearchParamConst::expected_sim_per_sec / 5.0)); + expensive_abort_checker.set_deterministic(interval); + } + while (true) + { + thread_state.is_out_of_mem = false; + if ((check_abort(thread_state) || expensive_abort_checker()) + && m_nu_simulations >= m_min_simulations) + break; + state.start_simulation(m_nu_simulations.fetch_add(1)); + play_in_tree(thread_state); + if (thread_state.is_out_of_mem) + break; + playout(thread_state); + state.evaluate_playout(simulation.eval); + thread_state.stat_len.add(double(simulation.moves.size())); + update_values(thread_state); + if (SearchParamConst::rave) + update_rave(thread_state); + if (SearchParamConst::use_lgr) + update_lgr(thread_state); + } +} + +template +inline auto SearchBase::select_child(const Node& node) -> const Node* +{ + auto children = m_tree.get_children(node); + LIBBOARDGAME_ASSERT(! children.empty()); + auto parent_count = node.get_visit_count(); + Float bias_factor = m_exploration_constant * sqrt(parent_count); + static_assert(SearchParamConst::child_min_count > 0, ""); + auto bias_limit = bias_factor / SearchParamConst::child_min_count; + auto i = children.begin(); + auto value = i->get_value() + bias_factor / i->get_value_count(); + auto best_value = value; + auto best_child = i; + auto limit = best_value - bias_limit; + while (++i != children.end()) + { + value = i->get_value(); + if (value <= limit) + continue; + value += bias_factor / i->get_value_count(); + if (value > best_value) + { + best_value = value; + best_child = i; + limit = best_value - bias_limit; + } + } + return best_child; +} + +template +auto SearchBase::select_final() const-> const Node* +{ + // Select the child with the highest number of wins + auto children = m_tree.get_children(m_tree.get_root()); + if (children.empty()) + return nullptr; + auto i = children.begin(); + auto best_child = i; + auto max_wins = i->get_value_count() * i->get_value(); + while (++i != children.end()) + { + auto wins = i->get_value_count() * i->get_value(); + if (wins > max_wins) + { + max_wins = wins; + best_child = i; + } + } + return best_child; +} + +template +bool SearchBase::select_move(Move& mv) const +{ + auto child = select_final(); + if (child) + { + mv = child->get_move(); + return true; + } + else + return false; +} + +template +void SearchBase::set_callback(function callback) +{ + m_callback = callback; +} + +template +void SearchBase::set_expand_threshold(Float n) +{ + m_expand_threshold = n; +} + +template +void SearchBase::set_expand_threshold_inc(Float n) +{ + m_expand_threshold_inc = n; +} + +template +void SearchBase::set_rave_parent_max(Float n) +{ + m_rave_parent_max = n; +} + +template +void SearchBase::set_rave_child_max(Float n) +{ + m_rave_child_max = n; +} + +template +void SearchBase::set_rave_weight(Float v) +{ + m_rave_weight = v; +} + +template +void SearchBase::set_reuse_subtree(bool enable) +{ + m_reuse_subtree = enable; +} + +template +void SearchBase::set_reuse_tree(bool enable) +{ + m_reuse_tree = enable; +} + +template +void SearchBase::update_lgr(ThreadState& thread_state) +{ + const auto& simulation = thread_state.simulation; + auto& eval = simulation.eval; + auto max_eval = eval[0]; + for (PlayerInt i = 1; i < m_nu_players; ++i) + max_eval = max(eval[i], max_eval); + array is_winner; + for (PlayerInt i = 0; i < m_nu_players; ++i) + // Note: this handles a draw as a win. Without additional information + // we cannot make a good decision how to handle draws and some + // experiments in Blokus Duo showed (with low confidence) that treating + // them as a win for both players is slightly better than treating them + // as a loss for both. + is_winner[i] = (eval[i] == max_eval); + auto& moves = simulation.moves; + auto nu_moves = moves.size(); + Move last = moves.get_unchecked(0).move; + Move second_last = Move::null(); + for (unsigned i = 1; i < nu_moves; ++i) + { + PlayerMove reply = moves[i]; + PlayerInt player = reply.player; + Move mv = reply.move; + if (is_winner[player]) + m_lgr.store(player, last, second_last, mv); + else + m_lgr.forget(player, last, second_last, mv); + second_last = last; + last = mv; + } +} + +template +void SearchBase::update_rave(ThreadState& thread_state) +{ + const auto& state = *thread_state.state; + auto& moves = thread_state.simulation.moves; + auto nu_moves = static_cast(moves.size()); + if (nu_moves == 0) + return; + auto& was_played = thread_state.was_played; + auto& first_play = thread_state.first_play; + auto& nodes = thread_state.simulation.nodes; + unsigned nu_nodes = static_cast(nodes.size()); + unsigned i = nu_moves - 1; + // nu_nodes is at least 2 (including root) because the case of no legal + // moves at the root is already handled before running any simulations. + LIBBOARDGAME_ASSERT(nu_nodes > 1); + + // Fill was_played and first_play with information from playout moves + for ( ; i >= nu_nodes - 1; --i) + { + auto mv = moves[i]; + if (state.skip_rave(mv.move)) + continue; + was_played[mv.move.to_int()] = mv.player; + first_play[mv.move.to_int()] = i; + } + + // Add RAVE values to children of nodes of current simulation + while (true) + { + const auto node = nodes[i]; + if (node->get_visit_count() > m_rave_parent_max) + break; + auto mv = moves[i]; + auto player = mv.player; + Float dist_factor; + if (SearchParamConst::rave_dist_weighting) + dist_factor = 1 / static_cast(nu_moves - i); + auto children = m_tree.get_children(*node); + LIBBOARDGAME_ASSERT(! children.empty()); + auto it = children.begin(); + do + { + auto mv = it->get_move(); + if (was_played[mv.to_int()] != player + || it->get_value_count() > m_rave_child_max) + continue; + auto first = first_play[mv.to_int()]; + LIBBOARDGAME_ASSERT(first > i); + Float weight = m_rave_weight; + if (SearchParamConst::rave_dist_weighting) + weight *= 1 - static_cast(first - i) * dist_factor; + m_tree.add_value(*it, thread_state.simulation.eval[player], weight); + } + while (++it != children.end()); + if (i == 0) + break; + if (! state.skip_rave(mv.move)) + { + was_played[mv.move.to_int()] = player; + first_play[mv.move.to_int()] = i; + } + --i; + } + + // Reset was_played + while (++i < nu_moves) + was_played[moves[i].move.to_int()] = max_players; +} + +template +void SearchBase::update_values(ThreadState& thread_state) +{ + const auto& simulation = thread_state.simulation; + auto& nodes = simulation.nodes; + auto& eval = simulation.eval; + unsigned nu_nodes = static_cast(nodes.size()); + m_tree.inc_visit_count(*nodes[0]); + for (unsigned i = 1; i < nu_nodes; ++i) + { + auto& node = *nodes[i]; + auto mv = simulation.moves[i - 1]; + if (multithread && SearchParamConst::virtual_loss) + // Note that this could become problematic if the number of threads + // is large. The lock-free algorithm intentionally ignores lost or + // partial updates to run faster. But the probability that adding + // a virtual loss is lost is not the same as that its removal is + // lost because the removal is done in this function with many + // calls to add_value() but the adding is done in play_in_tree(). + // This could introduce a systematic error. + m_tree.add_value_remove_loss(node, eval[mv.player]); + else + m_tree.add_value(node, eval[mv.player]); + m_tree.inc_visit_count(node); + } + for (PlayerInt i = 0; i < m_nu_players; ++i) + m_root_val[i].add(eval[i]); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_SEARCH_BASE_H diff --git a/src/libboardgame_mcts/Tree.h b/src/libboardgame_mcts/Tree.h new file mode 100644 index 0000000..ba5f0ed --- /dev/null +++ b/src/libboardgame_mcts/Tree.h @@ -0,0 +1,474 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/Tree.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_TREE_H +#define LIBBOARDGAME_MCTS_TREE_H + +#include +#include +#include "Node.h" +#include "libboardgame_util/Abort.h" +#include "libboardgame_util/IntervalChecker.h" + +namespace libboardgame_mcts { + +using namespace std; +using libboardgame_util::get_abort; +using libboardgame_util::IntervalChecker; + +//----------------------------------------------------------------------------- + +/** %Tree for Monte-Carlo tree search. + The nodes can be modified only through member functions of this class, + so that it can guarantee an intact tree structure. The user has access to + all nodes, but only as const references.

+ The tree uses separate parts of the node storage for different threads, + so it can be used without locking in multi-threaded search. Not all + functions are thread-safe, only the ones that are used during a search + (e.g. expanding a node is thread-safe, but clear() is not) */ +template +class Tree +{ + struct ThreadStorage; + + friend class NodeExpander; + +public: + typedef N Node; + + typedef typename Node::Move Move; + + typedef typename Node::Float Float; + + /** Range for iterating over the children of a node. */ + class Children + { + public: + Children(const Tree& tree, const Node& node) + { + auto nu_children = node.get_nu_children(); + m_begin = (nu_children != 0 ? + &tree.get_node(node.get_first_child()) : nullptr); + m_end = m_begin + nu_children; + } + + const Node* begin() const + { + return m_begin; + } + + const Node* end() const + { + return m_end; + } + + bool empty() const + { + return m_begin == nullptr; + } + + private: + const Node* m_begin; + + const Node* m_end; + }; + + + /** Helper class that is passed to the search state during node expansion. + This class allows the search state to directly create children of a + node at the node expansion, so that copying to a temporary move list + is not necessary, but avoids that the search needs to expose a + non-const reference to the tree to the state. */ + class NodeExpander + { + public: + /** Constructor. + @param thread_id + @param tree + @param child_min_count The minimum count used for initializing + children. Used only in debug mode to assert that the children + are really initialized with a minimum count as declared with + SearchParamConst::child_min_count. */ + NodeExpander(unsigned thread_id, Tree& tree, Float child_min_count); + + /** Check if the tree still has the capacity for a given number + of children. */ + bool check_capacity(unsigned short nu_children) const; + + /** Add new child. + It needs to be checked first with check_capacity() that the tree + has enough capacity. */ + void add_child(const Move& mv, Float value, Float count); + + /** Link the children to the parent node. */ + void link_children(Tree& tree, const Node& node); + + /** Return the node to play after the node expansion. + This returns the child with the highest value if prior knowledge + was used, or the first child, or null if no children. This can be + used for avoiding and extra iteration over the children when + selecting a child after a node expansion. */ + const Node* get_best_child() const; + + private: + ThreadStorage& m_thread_storage; + + Float m_best_value = -numeric_limits::max(); + + const Node* m_first_child; + + const Node* m_best_child; + +#if LIBBOARDGAME_DEBUG + Float m_child_min_count; +#endif + }; + + Tree(size_t memory, unsigned nu_threads); + + ~Tree(); + + /** Remove all nodes but the root node. */ + void clear(); + + const Node& get_root() const; + + Children get_children(const Node& node) const + { + return Children(*this, node); + } + + Children get_root_children() const + { + return get_children(get_root()); + } + + size_t get_nu_nodes() const; + + const Node& get_node(NodeIdx i) const; + + void link_children(const Node& node, const Node* first_child, + unsigned short nu_children); + + void add_value(const Node& node, Float v); + + void add_value(const Node& node, Float v, Float weight); + + void add_value_remove_loss(const Node& node, Float v); + + void inc_visit_count(const Node& node); + + void swap(Tree& tree); + + /** Extract a subtree. + Note that you still have to re-initialize the value of the subtree + after the extraction because the value of the root node and the values + of inner nodes have a different meaning. + @pre Target tree is empty (! target.get_root().has_children()) + @param target The target tree + @param node The root node of the subtree. */ + void extract_subtree(Tree& target, const Node& node) const; + + /** Copy a subtree. + The caller is responsible that the trees have the same number of + maximum nodes and that the target tree has room for the subtree. + @param target The target tree + @param target_node The target node + @param node The root node of the subtree. + @param min_count Don't copy subtrees of nodes below this count */ + void copy_subtree(Tree& target, const Node& target_node, const Node& node, + Float min_count) const; + +private: + struct ThreadStorage + { + Node* begin; + + Node* end; + + Node* next; + }; + + + unique_ptr m_nodes; + + unique_ptr m_thread_storage; + + unsigned m_nu_threads; + + size_t m_max_nodes; + + size_t m_nodes_per_thread; + + + bool contains(const Node& node) const; + + void copy_recurse(Tree& target, const Node& target_node, const Node& node, + Float min_count) const; + + unsigned get_thread_storage(const Node& node) const; + + Node& non_const(const Node& node) const; +}; + +template +inline Tree::NodeExpander::NodeExpander(unsigned thread_id, Tree& tree, + Float child_min_count) + : m_thread_storage(tree.m_thread_storage[thread_id]), + m_first_child(m_thread_storage.next), + m_best_child(nullptr) +{ + LIBBOARDGAME_ASSERT(thread_id < tree.m_nu_threads); +#if LIBBOARDGAME_DEBUG + m_child_min_count = child_min_count; +#else + LIBBOARDGAME_UNUSED(child_min_count); +#endif +} + +template +inline void Tree::NodeExpander::add_child(const Move& mv, Float value, + Float count) +{ + // -numeric_limits::max() ist init value for m_best_value + LIBBOARDGAME_ASSERT(value > -numeric_limits::max()); + LIBBOARDGAME_ASSERT(count >= m_child_min_count); + auto& next = m_thread_storage.next; + LIBBOARDGAME_ASSERT(next < m_thread_storage.end); + next->init(mv, value, count); + if (value > m_best_value) + { + m_best_child = next; + m_best_value = value; + } + ++next; +} + +template +inline bool Tree::NodeExpander::check_capacity( + unsigned short nu_children) const +{ + return m_thread_storage.end - m_thread_storage.next >= nu_children; +} + +template +inline auto Tree::NodeExpander::get_best_child() const -> const Node* +{ + return m_best_child; +} + +template +inline auto Tree::get_node(NodeIdx i) const -> const Node& +{ + return m_nodes[i]; +} + +template +inline void Tree::NodeExpander::link_children(Tree& tree, const Node& node) +{ + auto nu_children = + static_cast(m_thread_storage.next - m_first_child); + tree.link_children(node, m_first_child, nu_children); +} + + +template +Tree::Tree(size_t memory, unsigned nu_threads) + : m_nu_threads(nu_threads) +{ + size_t max_nodes = memory / sizeof(Node); + // It doesn't make sense to set max_nodes higher than what can be accessed + // with NodeIdx + max_nodes = + min(max_nodes, static_cast(numeric_limits::max())); + if (max_nodes == 0) + // We need at least the root node (for useful searches we need of + // course also children, but a root node is the minimum requirement to + // avoid crashing). + max_nodes = 1; + m_max_nodes = max_nodes; + m_nodes.reset(new Node[max_nodes]); + m_thread_storage.reset(new ThreadStorage[m_nu_threads]); + m_nodes_per_thread = max_nodes / m_nu_threads; + for (unsigned i = 0; i < m_nu_threads; ++i) + { + auto& thread_storage = m_thread_storage[i]; + thread_storage.begin = m_nodes.get() + i * m_nodes_per_thread; + thread_storage.end = thread_storage.begin + m_nodes_per_thread; + } + clear(); +} + +template +Tree::~Tree() = default; + +template +inline void Tree::add_value(const Node& node, Float v) +{ + non_const(node).add_value(v); +} + +template +inline void Tree::add_value(const Node& node, Float v, Float weight) +{ + non_const(node).add_value(v, weight); +} + +template +void Tree::clear() +{ + m_thread_storage[0].next = m_thread_storage[0].begin + 1; + for (unsigned i = 1; i < m_nu_threads; ++i) + m_thread_storage[i].next = m_thread_storage[i].begin; + m_nodes[0].init_root(); +} + +template +bool Tree::contains(const Node& node) const +{ + return &node >= m_nodes.get() && &node < m_nodes.get() + m_max_nodes; +} + +template +void Tree::copy_subtree(Tree& target, const Node& target_node, + const Node& node, Float min_count) const +{ + target.non_const(target_node).copy_data_from(node); + if (node.has_children()) + copy_recurse(target, target_node, node, min_count); + else + target.non_const(target_node).unlink_children_st(); +} + +template +void Tree::copy_recurse(Tree& target, const Node& target_node, + const Node& node, Float min_count) const +{ + LIBBOARDGAME_ASSERT(target.m_max_nodes == m_max_nodes); + LIBBOARDGAME_ASSERT(target.m_nu_threads == m_nu_threads); + LIBBOARDGAME_ASSERT(contains(node)); + auto nu_children = node.get_nu_children(); + auto& first_child = get_node(node.get_first_child()); + // Create target children in the equivalent thread storage as in source. + // This ensures that the thread storage will not overflow (because the + // trees have identical nu_threads/max_nodes) + ThreadStorage& thread_storage = + target.m_thread_storage[get_thread_storage(first_child)]; + auto target_child = thread_storage.next; + auto target_first_child = + static_cast(target_child - target.m_nodes.get()); + target.non_const(target_node).link_children_st(target_first_child, + nu_children); + thread_storage.next += nu_children; + // Without the extra () around thread_storage.next in the following + // assert, GCC 4.7.2 gives the error: parse error in template argument list + LIBBOARDGAME_ASSERT((thread_storage.next) < thread_storage.end); + auto end = &first_child + node.get_nu_children(); + for (auto i = &first_child; i != end; ++i, ++target_child) + { + target_child->copy_data_from(*i); + if (! i->has_children() || i->get_visit_count() < min_count) + { + target_child->unlink_children_st(); + continue; + } + copy_recurse(target, *target_child, *i, min_count); + } +} + +template +void Tree::extract_subtree(Tree& target, const Node& node) const +{ + LIBBOARDGAME_ASSERT(contains(node)); + LIBBOARDGAME_ASSERT(&target != this); + LIBBOARDGAME_ASSERT(target.m_max_nodes == m_max_nodes); + LIBBOARDGAME_ASSERT(! target.get_root().has_children()); + copy_subtree(target, target.m_nodes[0], node, 0); +} + +template +size_t Tree::get_nu_nodes() const +{ + size_t result = 0; + for (unsigned i = 0; i < m_nu_threads; ++i) + { + auto& thread_storage = m_thread_storage[i]; + result += thread_storage.next - thread_storage.begin; + } + return result; +} + +template +inline auto Tree::get_root() const -> const Node& +{ + return m_nodes[0]; +} + +/** Get the thread storage a node belongs to. */ +template +inline unsigned Tree::get_thread_storage(const Node& node) const +{ + size_t diff = &node - m_nodes.get(); + return static_cast(diff / m_nodes_per_thread); +} + +template +inline void Tree::inc_visit_count(const Node& node) +{ + non_const(node).inc_visit_count(); +} + +template +inline void Tree::link_children(const Node& node, const Node* first_child, + unsigned short nu_children) +{ + NodeIdx first_child_idx = static_cast(first_child - m_nodes.get()); + LIBBOARDGAME_ASSERT(first_child_idx > 0); + LIBBOARDGAME_ASSERT(first_child_idx < m_max_nodes); + non_const(node).link_children(first_child_idx, nu_children); +} + +/** Convert a const reference to node from user to a non-const reference. + The user has only read access to the nodes, because the tree guarantees + the validity of the tree structure. */ +template +inline auto Tree::non_const(const Node& node) const -> Node& +{ + LIBBOARDGAME_ASSERT(contains(node)); + return const_cast(node); +} + +template +inline void Tree::add_value_remove_loss(const Node& node, Float v) +{ + non_const(node).add_value_remove_loss(v); +} + +template +void Tree::swap(Tree& tree) +{ + // Reminder to update this function when the class gets additional members + struct Dummy + { + unsigned m_nu_threads; + size_t m_max_nodes; + size_t m_nodes_per_thread; + unique_ptr m_thread_storage; + unique_ptr m_nodes; + }; + static_assert(sizeof(Tree) == sizeof(Dummy), ""); + std::swap(m_nu_threads, tree.m_nu_threads); + std::swap(m_max_nodes, tree.m_max_nodes); + std::swap(m_nodes_per_thread, tree.m_nodes_per_thread); + m_thread_storage.swap(tree.m_thread_storage); + m_nodes.swap(tree.m_nodes); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_TREE_H diff --git a/src/libboardgame_mcts/TreeUtil.h b/src/libboardgame_mcts/TreeUtil.h new file mode 100644 index 0000000..6b64efe --- /dev/null +++ b/src/libboardgame_mcts/TreeUtil.h @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/TreeUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_TREE_UTIL_H +#define LIBBOARDGAME_MCTS_TREE_UTIL_H + +#include "Tree.h" + +namespace libboardgame_mcts { +namespace tree_util { + +//----------------------------------------------------------------------------- + +template +const N* find_child(const Tree& tree, const N& node, typename N::Move mv) +{ + for (auto& i : tree.get_children(node)) + if (i.get_move() == mv) + return &i; + return nullptr; +} + +template +const N* find_node(const Tree& tree, const S& sequence) +{ + auto node = &tree.get_root(); + for (auto mv : sequence) + if (! ((node = find_child(tree, *node, mv)))) + break; + return node; +} + +//----------------------------------------------------------------------------- + +} // namespace tree_util +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_TREE_UTIL_H diff --git a/src/libboardgame_sgf/CMakeLists.txt b/src/libboardgame_sgf/CMakeLists.txt new file mode 100644 index 0000000..0cb8af9 --- /dev/null +++ b/src/libboardgame_sgf/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(boardgame_sgf STATIC + InvalidPropertyValue.h + InvalidTree.h + MissingProperty.h + MissingProperty.cpp + Reader.h + Reader.cpp + SgfNode.h + SgfNode.cpp + SgfTree.h + SgfTree.cpp + SgfUtil.h + SgfUtil.cpp + TreeReader.h + TreeReader.cpp + TreeWriter.h + TreeWriter.cpp + Writer.h + Writer.cpp +) diff --git a/src/libboardgame_sgf/InvalidPropertyValue.h b/src/libboardgame_sgf/InvalidPropertyValue.h new file mode 100644 index 0000000..7fc7e54 --- /dev/null +++ b/src/libboardgame_sgf/InvalidPropertyValue.h @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/InvalidPropertyValue.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_INVALID_PROPERTY_VALUE_H +#define LIBBOARDGAME_SGF_INVALID_PROPERTY_VALUE_H + +#include "InvalidTree.h" + +#include "sstream" + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +class InvalidPropertyValue + : public InvalidTree +{ +public: + template + InvalidPropertyValue(const string& id, const T& value); + +private: + template + static string get_message(const string& id, const T& value); +}; + +template +InvalidPropertyValue::InvalidPropertyValue(const string& id, const T& value) + : InvalidTree(get_message(id, value)) +{ +} + +template +string InvalidPropertyValue::get_message(const string& id, const T& value) +{ + ostringstream msg; + msg << "Invalid value '" << value << " for SGF property '" << id << "'"; + return msg.str(); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_INVALID_PROPERTY_VALUE_H diff --git a/src/libboardgame_sgf/InvalidTree.h b/src/libboardgame_sgf/InvalidTree.h new file mode 100644 index 0000000..1bda509 --- /dev/null +++ b/src/libboardgame_sgf/InvalidTree.h @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/InvalidTree.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_INVALID_TREE_H +#define LIBBOARDGAME_SGF_INVALID_TREE_H + +#include + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Exception indication a semantic error in the tree. + This exception is used for semantic errors in SGF trees. If a SGF tree + is loaded from an external file, it is usually only checked for + (game-independent) syntax errors, but not for semantic errors (e.g. illegal + moves) because that would be too expensive when loading large trees and + not allow the user to partially use a tree if there is an error only in + some variations. As a consequence, functions that use the tree may cause + errors later (e.g. when trying to update the game state to a node in the + tree). In this case, they should throw InvalidTree. */ +class InvalidTree + : public runtime_error +{ + using runtime_error::runtime_error; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_INVALID_TREE_H diff --git a/src/libboardgame_sgf/MissingProperty.cpp b/src/libboardgame_sgf/MissingProperty.cpp new file mode 100644 index 0000000..eeb3a4b --- /dev/null +++ b/src/libboardgame_sgf/MissingProperty.cpp @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/MissingProperty.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "MissingProperty.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +MissingProperty::MissingProperty(const string& message) + : InvalidTree("Missing SGF property: " + message) +{ +} + +MissingProperty::MissingProperty(const string& id, const string& message) + : InvalidTree("Missing SGF property '" + id + ": " + message) +{ +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/MissingProperty.h b/src/libboardgame_sgf/MissingProperty.h new file mode 100644 index 0000000..83fbb3c --- /dev/null +++ b/src/libboardgame_sgf/MissingProperty.h @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/MissingProperty.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_MISSING_PROPERTY_H +#define LIBBOARDGAME_SGF_MISSING_PROPERTY_H + +#include "InvalidTree.h" + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +class MissingProperty + : public InvalidTree +{ +public: + explicit MissingProperty(const string& message); + + MissingProperty(const string& id, const string& message); +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_MISSING_PROPERTY_H diff --git a/src/libboardgame_sgf/Reader.cpp b/src/libboardgame_sgf/Reader.cpp new file mode 100644 index 0000000..f0004d7 --- /dev/null +++ b/src/libboardgame_sgf/Reader.cpp @@ -0,0 +1,261 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/Reader.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Reader.h" + +#include +#include +#include +#include "libboardgame_util/Assert.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +namespace { + +/** Replacement for std::isspace() that returns true only for whitespaces + in the ASCII range. */ +bool is_ascii_space(int c) +{ + return c >= 0 && c < 128 && isspace(c); +} + +} // namespace + +//----------------------------------------------------------------------------- + +Reader::Reader() = default; + +Reader::~Reader() = default; + +void Reader::consume_char(char expected) +{ + LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(expected); + char c = read_char(); + LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(c); + LIBBOARDGAME_ASSERT(c == expected); +} + +void Reader::consume_whitespace() +{ + while (is_ascii_space(peek())) + m_in->get(); +} + +void Reader::on_begin_node(bool is_root) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(is_root); +} + +void Reader::on_begin_tree(bool is_root) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(is_root); +} + +void Reader::on_end_node() +{ + // Default implementation does nothing +} + +void Reader::on_end_tree(bool is_root) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(is_root); +} + +void Reader::on_property(const string& id, const vector& values) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(id); + LIBBOARDGAME_UNUSED(values); +} + +char Reader::peek() +{ + int c = m_in->peek(); + if (c == EOF) + throw ReadError("Unexpected end of input"); + return char(c); +} + +void Reader::read(istream& in, bool check_single_tree, + bool* more_game_trees_left) +{ + m_in = ∈ + m_is_in_main_variation = true; + consume_whitespace(); + read_tree(true); + while (true) + { + int c = m_in->peek(); + if (c == EOF) + { + if (more_game_trees_left) + *more_game_trees_left = false; + return; + } + else if (c == '(') + { + if (check_single_tree) + throw ReadError("Input has multiple game trees"); + else + { + if (more_game_trees_left) + *more_game_trees_left = true; + return; + } + } + else if (is_ascii_space(c)) + m_in->get(); + else + throw ReadError("Extra characters after end of tree."); + } +} + +void Reader::read(const string& file) +{ + ifstream in(file); + if (! in) + throw ReadError("Could not open '" + file + "'"); + try + { + read(in, true); + } + catch (const ReadError& e) + { + throw ReadError("Could not read '" + file + "': " + e.what()); + } +} + +char Reader::read_char() +{ + int c = m_in->get(); + if (c == EOF) + throw ReadError("Unexpected end of SGF stream"); + if (c == '\r') + { + // Convert CR+LF or single CR into LF + if (peek() == '\n') + m_in->get(); + return '\n'; + } + return char(c); +} + +void Reader::read_expected(char expected) +{ + if (read_char() != expected) + throw ReadError(string("Expected '") + expected + "'"); +} + +void Reader::read_node(bool is_root) +{ + read_expected(';'); + if (! m_read_only_main_variation || m_is_in_main_variation) + on_begin_node(is_root); + while (true) + { + consume_whitespace(); + char c = peek(); + if (c == '(' || c == ')' || c == ';') + break; + read_property(); + } + if (! m_read_only_main_variation || m_is_in_main_variation) + on_end_node(); +} + +void Reader::read_property() +{ + if (m_read_only_main_variation && ! m_is_in_main_variation) + { + while (peek() != '[') + read_char(); + while (peek() == '[') + { + consume_char('['); + bool escape = false; + while (peek() != ']' || escape) + { + char c = read_char(); + if (c == '\\' && ! escape) + { + escape = true; + continue; + } + escape = false; + } + consume_char(']'); + consume_whitespace(); + } + } + else + { + m_id.clear(); + while (peek() != '[') + m_id += read_char(); + m_values.clear(); + while (peek() == '[') + { + consume_char('['); + m_value.clear(); + bool escape = false; + while (peek() != ']' || escape) + { + char c = read_char(); + if (c == '\\' && ! escape) + { + escape = true; + continue; + } + escape = false; + m_value += c; + } + consume_char(']'); + consume_whitespace(); + m_values.push_back(m_value); + } + on_property(m_id, m_values); + } +} + +void Reader::read_tree(bool is_root) +{ + read_expected('('); + on_begin_tree(is_root); + bool was_root = is_root; + while (true) + { + consume_whitespace(); + char c = peek(); + if (c == ')') + break; + else if (c == ';') + { + read_node(is_root); + is_root = false; + } + else if (c == '(') + read_tree(false); + else + throw ReadError("Extra text before node"); + } + read_expected(')'); + m_is_in_main_variation = false; + on_end_tree(was_root); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/Reader.h b/src/libboardgame_sgf/Reader.h new file mode 100644 index 0000000..34d005d --- /dev/null +++ b/src/libboardgame_sgf/Reader.h @@ -0,0 +1,110 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/Reader.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_READER_H +#define LIBBOARDGAME_SGF_READER_H + +#include +#include +#include +#include + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +class Reader +{ +public: + class ReadError + : public runtime_error + { + using runtime_error::runtime_error; + }; + + Reader(); + + virtual ~Reader(); + + virtual void on_begin_tree(bool is_root); + + virtual void on_end_tree(bool is_root); + + virtual void on_begin_node(bool is_root); + + virtual void on_end_node(); + + virtual void on_property(const string& id, const vector& values); + + /** Read only the main variation. + Reduces CPU time and memory if only the main variation is needed. */ + void set_read_only_main_variation(bool enable); + + /** Read a game tree from the file. + @param in + @param check_single_tree Throw an error if non-whitespace characters + follow after the tree before the end of the stream. This is mainly + useful to ensure that the input is not a SGF file with multiple game + trees if the caller does not want to handle this case. If + check_single_tree is false, you can call read() multiple times to read + all game trees. + @param[out] more_game_trees_left set to true if check_single_tree is + false and there are more game trees to read. + @throws ReadError */ + void read(istream& in, bool check_single_tree = true, + bool* more_game_trees_left = nullptr); + + /** See read(istream&,bool) */ + void read(const string& file); + +private: + bool m_read_only_main_variation = false; + + bool m_is_in_main_variation; + + istream* m_in; + + /** Local variable in read_property(). + Reused for efficiency. */ + string m_id; + + /** Local variable in read_property(). + Reused for efficiency. */ + string m_value; + + /** Local variable in read_property(). + Reused for efficiency. */ + vector m_values; + + void consume_char(char expected); + + void consume_whitespace(); + + char peek(); + + char read_char(); + + void read_expected(char expected); + + void read_node(bool is_root); + + void read_property(); + + void read_tree(bool is_root); +}; + +inline void Reader::set_read_only_main_variation(bool enable) +{ + m_read_only_main_variation = enable; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_READER_H diff --git a/src/libboardgame_sgf/SgfNode.cpp b/src/libboardgame_sgf/SgfNode.cpp new file mode 100644 index 0000000..5d0e148 --- /dev/null +++ b/src/libboardgame_sgf/SgfNode.cpp @@ -0,0 +1,298 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfNode.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SgfNode.h" + +#include +#include "MissingProperty.h" +#include "libboardgame_util/Assert.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +Property::~Property() = default; + +//----------------------------------------------------------------------------- + +SgfNode::SgfNode() = default; + +SgfNode::~SgfNode() = default; + +void SgfNode::append(unique_ptr node) +{ + node->m_parent = this; + if (! m_first_child) + m_first_child = move(node); + else + get_last_child()->m_sibling = move(node); +} + +SgfNode& SgfNode::create_new_child() +{ + unique_ptr node(new SgfNode); + node->m_parent = this; + SgfNode& result = *(node.get()); + auto last_child = get_last_child(); + if (! last_child) + m_first_child = move(node); + else + last_child->m_sibling = move(node); + return result; +} + +void SgfNode::delete_variations() +{ + if (m_first_child) + m_first_child->m_sibling.reset(nullptr); +} + +forward_list::const_iterator SgfNode::find_property( + const string& id) const +{ + return find_if(m_properties.begin(), m_properties.end(), + [&](const Property& p) { return p.id == id; }); +} + +const vector SgfNode::get_multi_property(const string& id) const +{ + auto property = find_property(id); + if (property == m_properties.end()) + return vector(); + else + return property->values; +} + +bool SgfNode::has_property(const string& id) const +{ + return find_property(id) != m_properties.end(); +} + +const SgfNode& SgfNode::get_child(unsigned i) const +{ + LIBBOARDGAME_ASSERT(i < get_nu_children()); + auto child = m_first_child.get(); + while (i > 0) + { + child = child->m_sibling.get(); + --i; + } + return *child; +} + +unsigned SgfNode::get_child_index(const SgfNode& child) const +{ + auto current = m_first_child.get(); + unsigned i = 0; + while (true) + { + if (current == &child) + return i; + current = current->m_sibling.get(); + LIBBOARDGAME_ASSERT(current); + ++i; + } +} + +SgfNode* SgfNode::get_last_child() const +{ + auto node = m_first_child.get(); + if (! node) + return nullptr; + while (node->m_sibling) + node = node->m_sibling.get(); + return node; +} + +unsigned SgfNode::get_nu_children() const +{ + unsigned n = 0; + auto child = m_first_child.get(); + while (child) + { + ++n; + child = child->m_sibling.get(); + } + return n; +} + +const SgfNode* SgfNode::get_previous_sibling() const +{ + if (! m_parent) + return nullptr; + auto child = &m_parent->get_first_child(); + if (child == this) + return nullptr; + do + { + if (child->get_sibling() == this) + return child; + child = child->get_sibling(); + } + while (child); + LIBBOARDGAME_ASSERT(false); + return nullptr; +} + +const string& SgfNode::get_property(const string& id) const +{ + auto property = find_property(id); + if (property == m_properties.end()) + throw MissingProperty(id); + return property->values[0]; +} + +const string& SgfNode::get_property(const string& id, + const string& default_value) const +{ + auto property = find_property(id); + if (property == m_properties.end()) + return default_value; + else + return property->values[0]; +} + +void SgfNode::make_first_child() +{ + LIBBOARDGAME_ASSERT(has_parent()); + auto current_child = m_parent->m_first_child.get(); + if (current_child == this) + return; + while (true) + { + auto sibling = current_child->m_sibling.get(); + if (sibling == this) + { + unique_ptr tmp = move(m_parent->m_first_child); + m_parent->m_first_child = move(current_child->m_sibling); + current_child->m_sibling = move(m_sibling); + m_sibling = move(tmp); + return; + } + current_child = sibling; + } +} + +bool SgfNode::move_property_to_front(const string& id) +{ + auto i = m_properties.begin(); + forward_list::const_iterator previous = m_properties.end(); + for ( ; i != m_properties.end(); ++i) + if (i->id == id) + break; + else + previous = i; + if (i == m_properties.begin() || i == m_properties.end()) + return false; + auto property = *i; + m_properties.erase_after(previous); + m_properties.push_front(property); + return true; +} + +void SgfNode::move_down() +{ + LIBBOARDGAME_ASSERT(has_parent()); + auto current = m_parent->m_first_child.get(); + if (current == this) + { + unique_ptr tmp = move(m_parent->m_first_child); + m_parent->m_first_child = move(m_sibling); + m_sibling = move(m_parent->m_first_child->m_sibling); + m_parent->m_first_child->m_sibling = move(tmp); + return; + } + while (true) + { + auto sibling = current->m_sibling.get(); + if (sibling == this) + { + if (! m_sibling) + return; + unique_ptr tmp = move(current->m_sibling); + current->m_sibling = move(m_sibling); + m_sibling = move(current->m_sibling->m_sibling); + current->m_sibling->m_sibling = move(tmp); + return; + } + current = sibling; + } +} + +void SgfNode::move_up() +{ + LIBBOARDGAME_ASSERT(has_parent()); + auto current = m_parent->m_first_child.get(); + if (current == this) + return; + SgfNode* prev = nullptr; + while (true) + { + auto sibling = current->m_sibling.get(); + if (sibling == this) + { + if (! prev) + { + make_first_child(); + return; + } + unique_ptr tmp = move(prev->m_sibling); + prev->m_sibling = move(current->m_sibling); + current->m_sibling = move(m_sibling); + m_sibling = move(tmp); + return; + } + prev = current; + current = sibling; + } +} + +bool SgfNode::remove_property(const string& id) +{ + forward_list::const_iterator previous = m_properties.end(); + for (auto i = m_properties.begin() ; i != m_properties.end(); ++i) + if (i->id == id) + { + if (previous == m_properties.end()) + m_properties.pop_front(); + else + m_properties.erase_after(previous); + return true; + } + else + previous = i; + return false; +} + +unique_ptr SgfNode::remove_child(SgfNode& child) +{ + auto node = &m_first_child; + unique_ptr* previous = nullptr; + while (true) + { + if (node->get() == &child) + { + unique_ptr result = move(*node); + if (! previous) + m_first_child = move(child.m_sibling); + else + (*previous)->m_sibling = move(child.m_sibling); + result->m_parent = nullptr; + return result; + } + previous = node; + node = &(*node)->m_sibling; + LIBBOARDGAME_ASSERT(node); + } +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/SgfNode.h b/src/libboardgame_sgf/SgfNode.h new file mode 100644 index 0000000..b7f0e60 --- /dev/null +++ b/src/libboardgame_sgf/SgfNode.h @@ -0,0 +1,393 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfNode.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_SGF_NODE_H +#define LIBBOARDGAME_SGF_SGF_NODE_H + +#include +#include +#include +#include +#include "InvalidPropertyValue.h" +#include "libboardgame_util/Assert.h" +#include "libboardgame_util/StringUtil.h" + +namespace libboardgame_sgf { + +using namespace std; +using libboardgame_util::from_string; +using libboardgame_util::to_string; + +//----------------------------------------------------------------------------- + +struct Property +{ + string id; + + vector values; + + unique_ptr next; + + Property(const Property& p) + : id(p.id), + values(p.values) + { } + + Property(const string& id, const vector& values) + : id(id), + values(values) + { + LIBBOARDGAME_ASSERT(! id.empty()); + LIBBOARDGAME_ASSERT(! values.empty()); + } + + ~Property(); +}; + +//----------------------------------------------------------------------------- + +class SgfNode +{ +public: + /** Iterates over siblings. */ + class Iterator + { + public: + explicit Iterator(const SgfNode* node) + { + m_node = node; + } + + bool operator==(Iterator it) const + { + return m_node == it.m_node; + } + + bool operator!=(Iterator it) const + { + return m_node != it.m_node; + } + + Iterator& operator++() + { + m_node = m_node->get_sibling(); + return *this; + } + + const SgfNode& operator*() const + { + return *m_node; + } + + const SgfNode* operator->() const + { + return m_node; + } + + bool is_null() const + { + return m_node == nullptr; + } + + private: + const SgfNode* m_node; + }; + + /** Range for iterating over the children of a node. */ + class Children + { + public: + explicit Children(const SgfNode& node) + : m_begin(node.get_first_child_or_null()) + { } + + Iterator begin() const { return m_begin; } + + Iterator end() const { return Iterator(nullptr); } + + bool empty() const { return m_begin.is_null(); } + + private: + Iterator m_begin; + }; + + SgfNode(); + + ~SgfNode(); + + /** Append a new child. */ + void append(unique_ptr node); + + bool has_property(const string& id) const; + + /** Get a property. + @pre has_property(id) */ + const string& get_property(const string& id) const; + + const string& get_property(const string& id, + const string& default_value) const; + + const vector get_multi_property(const string& id) const; + + /** Get property parsed as a type. + @pre has_property(id) + @throws InvalidPropertyValue, MissingProperty */ + template + T parse_property(const string& id) const; + + /** Get property parsed as a type with default value. + @throws InvalidPropertyValue */ + template + T parse_property(const string& id, const T& default_value) const; + + /** @return true, if property was added or changed. */ + template + bool set_property(const string& id, const T& value); + + /** @return true, if property was added or changed. */ + bool set_property(const string& id, const char* value); + + /** @return true, if property was added or changed. */ + template + bool set_property(const string& id, const vector& values); + + /** @return true, if node contained the property. */ + bool remove_property(const string& id); + + /** @return true, if the property was found and not already at the + front. */ + bool move_property_to_front(const string& id); + + const forward_list& get_properties() const + { + return m_properties; + } + + Children get_children() const + { + return Children(*this); + } + + SgfNode* get_sibling(); + + SgfNode& get_first_child(); + + const SgfNode& get_first_child() const; + + SgfNode* get_first_child_or_null(); + + const SgfNode* get_first_child_or_null() const; + + const SgfNode* get_sibling() const; + + const SgfNode* get_previous_sibling() const; + + bool has_children() const; + + bool has_single_child() const; + + unsigned get_nu_children() const; + + /** @pre i < get_nu_children() */ + const SgfNode& get_child(unsigned i) const; + + unsigned get_child_index(const SgfNode& child) const; + + /** Get single child. + @pre has_single_child() */ + const SgfNode& get_child() const; + + bool has_parent() const; + + /** Get parent node. + @pre has_parent() */ + const SgfNode& get_parent() const; + + /** Get parent node or null if node has no parent. */ + const SgfNode* get_parent_or_null() const; + + SgfNode& get_parent(); + + SgfNode& create_new_child(); + + /** Remove a child. + @return The removed child node. */ + unique_ptr remove_child(SgfNode& child); + + /** Remove all children. + @return A pointer to the first child (which also owns its siblings), + which can be used to append the children to a different node. */ + unique_ptr remove_children(); + + /** @pre has_parent() */ + void make_first_child(); + + /** Switch place with previous sibling. + If the node is already the first child, nothing happens. + @pre has_parent() */ + void move_up(); + + /** Switch place with sibling. + If the node is the last sibling, nothing happens. + @pre has_parent() */ + void move_down(); + + /** Delete all siblings of the first child. */ + void delete_variations(); + +private: + SgfNode* m_parent = nullptr; + + unique_ptr m_first_child; + + unique_ptr m_sibling; + + /** The properties. + Often a node has only one property (the move), so it saves memory + to use a forward_list instead of a vector. */ + forward_list m_properties; + + forward_list::const_iterator find_property( + const string& id) const; + + SgfNode* get_last_child() const; +}; + +inline const SgfNode& SgfNode::get_child() const +{ + LIBBOARDGAME_ASSERT(has_single_child()); + return *m_first_child; +} + +inline const SgfNode& SgfNode::get_parent() const +{ + LIBBOARDGAME_ASSERT(has_parent()); + return *m_parent; +} + +inline SgfNode& SgfNode::get_parent() +{ + LIBBOARDGAME_ASSERT(has_parent()); + return *m_parent; +} + +inline const SgfNode* SgfNode::get_parent_or_null() const +{ + return m_parent; +} + +inline SgfNode& SgfNode::get_first_child() +{ + LIBBOARDGAME_ASSERT(has_children()); + return *m_first_child.get(); +} + +inline const SgfNode& SgfNode::get_first_child() const +{ + LIBBOARDGAME_ASSERT(has_children()); + return *(m_first_child.get()); +} + +inline SgfNode* SgfNode::get_first_child_or_null() +{ + return m_first_child.get(); +} + +inline const SgfNode* SgfNode::get_first_child_or_null() const +{ + return m_first_child.get(); +} + +inline SgfNode* SgfNode::get_sibling() +{ + return m_sibling.get(); +} + +inline const SgfNode* SgfNode::get_sibling() const +{ + return m_sibling.get(); +} + +inline bool SgfNode::has_children() const +{ + return static_cast(m_first_child); +} + +inline bool SgfNode::has_parent() const +{ + return m_parent != nullptr; +} + +inline bool SgfNode::has_single_child() const +{ + return m_first_child && ! m_first_child->m_sibling; +} + +template +T SgfNode::parse_property(const string& id) const +{ + string value = get_property(id); + T result; + if (! from_string(value, result)) + throw InvalidPropertyValue(id, value); + return result; +} + +template +T SgfNode::parse_property(const string& id, const T& default_value) const +{ + if (! has_property(id)) + return default_value; + return parse_property(id); +} + +inline unique_ptr SgfNode::remove_children() +{ + if (m_first_child) + m_first_child->m_parent = nullptr; + return move(m_first_child); +} + +template +bool SgfNode::set_property(const string& id, const T& value) +{ + vector values(1, value); + return set_property(id, values); +} + +inline bool SgfNode::set_property(const string& id, const char* value) +{ + return set_property(id, value); +} + +template +bool SgfNode::set_property(const string& id, const vector& values) +{ + vector values_to_string; + for (const T& v : values) + values_to_string.push_back(to_string(v)); + forward_list::const_iterator last = m_properties.end(); + for (auto i = m_properties.begin(); i != m_properties.end(); ++i) + if (i->id == id) + { + bool was_changed = (i->values != values_to_string); + i->values = values_to_string; + return was_changed; + } + else + last = i; + if (last == m_properties.end()) + m_properties.emplace_front(id, values_to_string); + else + m_properties.emplace_after(last, id, values_to_string); + return true; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_SGF_NODE_H diff --git a/src/libboardgame_sgf/SgfTree.cpp b/src/libboardgame_sgf/SgfTree.cpp new file mode 100644 index 0000000..f2c1886 --- /dev/null +++ b/src/libboardgame_sgf/SgfTree.cpp @@ -0,0 +1,265 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfTree.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SgfTree.h" + +#include +#include +#include +#include "libboardgame_sgf/SgfUtil.h" + +namespace libboardgame_sgf { + +using libboardgame_sgf::util::find_root; +using libboardgame_util::trim; + +//----------------------------------------------------------------------------- + +SgfTree::SgfTree() +{ + init(); +} + +SgfTree::~SgfTree() +{ +} + +bool SgfTree::contains(const SgfNode& node) const +{ + return &find_root(node) == &get_root(); +} + +const SgfNode& SgfTree::create_new_child(const SgfNode& node) +{ + m_modified = true; + return non_const(node).create_new_child(); +} + +void SgfTree::delete_all_variations() +{ + if (has_variations()) + m_modified = true; + auto node = &get_root(); + while (node) + { + non_const(*node).delete_variations(); + node = node->get_first_child_or_null(); + } +} + +double SgfTree::get_bad_move(const SgfNode& node) +{ + return node.parse_property("BM", 0); +} + +string SgfTree::get_comment(const SgfNode& node) const +{ + return node.get_property("C", ""); +} + +string SgfTree::get_date_today() +{ + time_t t = time(nullptr); + auto tmp = localtime(&t); + if (! tmp) + return "?"; + char date[128]; + strftime(date, sizeof(date), "%Y-%m-%d", tmp); + return date; +} + +double SgfTree::get_good_move(const SgfNode& node) +{ + return node.parse_property("TE", 0); +} + +unique_ptr SgfTree::get_tree_transfer_ownership() +{ + return move(m_root); +} + +bool SgfTree::has_variations() const +{ + auto node = m_root.get(); + while (node) + { + if (node->get_sibling()) + return true; + node = node->get_first_child_or_null(); + } + return false; +} + +void SgfTree::init() +{ + unique_ptr root(new SgfNode); + m_root = move(root); + m_modified = false; +} + +void SgfTree::init(unique_ptr& root) +{ + m_root = move(root); + m_modified = false; +} + +bool SgfTree::is_doubtful_move(const SgfNode& node) +{ + return node.has_property("DO"); +} + +bool SgfTree::is_interesting_move(const SgfNode& node) +{ + return node.has_property("IT"); +} + +void SgfTree::make_first_child(const SgfNode& node) +{ + auto parent = node.get_parent_or_null(); + if (parent && &parent->get_first_child() != &node) + { + non_const(node).make_first_child(); + m_modified = true; + } +} + +void SgfTree::make_main_variation(const SgfNode& node) +{ + auto current = &non_const(node); + while (current->has_parent()) + { + make_first_child(*current); + current = ¤t->get_parent(); + } +} + +void SgfTree::make_root(const SgfNode& node) +{ + if (&node == &get_root()) + return; + LIBBOARDGAME_ASSERT(contains(node)); + auto& parent = node.get_parent(); + unique_ptr new_root = non_const(parent).remove_child(non_const(node)); + m_root = move(new_root); + m_modified = true; +} + +void SgfTree::move_property_to_front(const SgfNode& node, const string& id) +{ + if (non_const(node).move_property_to_front(id)) + m_modified = true; +} + +void SgfTree::move_down(const SgfNode& node) +{ + if (node.get_sibling()) + { + non_const(node).move_down(); + m_modified = true; + } +} + +void SgfTree::move_up(const SgfNode& node) +{ + auto parent = node.get_parent_or_null(); + if (parent && &parent->get_first_child() != &node) + { + non_const(node).move_up(); + m_modified = true; + } +} + +void SgfTree::remove_move_annotation(const SgfNode& node) +{ + remove_property(node, "BM"); + remove_property(node, "DO"); + remove_property(node, "IT"); + remove_property(node, "TE"); +} + +bool SgfTree::remove_property(const SgfNode& node, const string& id) +{ + bool prop_existed = non_const(node).remove_property(id); + if (prop_existed) + m_modified = true; + return prop_existed; +} + +void SgfTree::set_application(const string& name, const string& version) +{ + if (version.empty()) + set_property(get_root(), "AP", name); + else + set_property(get_root(), "AP", name + ":" + version); +} + +void SgfTree::set_property(const SgfNode& node, const string& id, const char* value) +{ + bool was_changed = non_const(node).set_property(id, value); + if (was_changed) + m_modified = true; +} + +void SgfTree::set_property_remove_empty(const SgfNode& node, const string& id, + const string& value) +{ + string trimmed = trim(value); + if (trimmed.empty()) + remove_property(node, id); + else + set_property(node, id, value); +} + +void SgfTree::set_bad_move(const SgfNode& node, double value) +{ + remove_move_annotation(node); + set_property(node, "BM", value); +} + +void SgfTree::set_comment(const SgfNode& node, const string& s) +{ + set_property_remove_empty(node, "C", s); +} + +void SgfTree::set_date_today() +{ + set_date(get_date_today()); +} + +void SgfTree::set_doubtful_move(const SgfNode& node) +{ + remove_move_annotation(node); + set_property(node, "DO", ""); +} + +void SgfTree::set_good_move(const SgfNode& node, double value) +{ + remove_move_annotation(node); + set_property(node, "TE", value); +} + +void SgfTree::set_interesting_move(const SgfNode& node) +{ + remove_move_annotation(node); + set_property(node, "IT", ""); +} + +const SgfNode& SgfTree::truncate(const SgfNode& node) +{ + LIBBOARDGAME_ASSERT(node.has_parent()); + auto& parent = node.get_parent(); + non_const(parent).remove_child(non_const(node)); + m_modified = true; + return parent; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/SgfTree.h b/src/libboardgame_sgf/SgfTree.h new file mode 100644 index 0000000..8b9a4f9 --- /dev/null +++ b/src/libboardgame_sgf/SgfTree.h @@ -0,0 +1,279 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfTree.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_SGF_TREE_H +#define LIBBOARDGAME_SGF_SGF_TREE_H + +#include "libboardgame_sgf/SgfNode.h" +#include "libboardgame_util/StringUtil.h" + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** SGF tree. + Tree structure of the tree can only be manipulated through member functions + to guarantee a consistent tree structure. Therefore the user is given + only const references to nodes and non-const functions of nodes can only + be called through wrapper functions of the tree (in which case the user + passes in a const reference to the node as an identifier for the node). */ +class SgfTree +{ +public: + SgfTree(); + + virtual ~SgfTree(); + + virtual void init(); + + /** Initialize from an existing SGF tree. + @param root The root node of the SGF tree; the ownership is transferred + to this class. */ + virtual void init(unique_ptr& root); + + /** Get the root node and transfer the ownership to the caller. */ + unique_ptr get_tree_transfer_ownership(); + + /** Check if the tree was modified since the construction or the last call + to init() or clear_modified() */ + bool is_modified() const; + + void set_modified(); + + void clear_modified(); + + const SgfNode& get_root() const; + + const SgfNode& create_new_child(const SgfNode& node); + + /** Truncate a node and its subtree from the tree. + Calling this function deletes the node that is to be truncated and its + complete subtree. + @pre node.has_parent() + @param node The node to be truncated. + @return The parent of the truncated node. */ + const SgfNode& truncate(const SgfNode& node); + + /** Delete all variations but the main variation. */ + void delete_all_variations(); + + /** Make a node the first child of its parent. */ + void make_first_child(const SgfNode& node); + + /** Make a node switch place with its previous sibling (if it is not + already the first child). */ + void move_up(const SgfNode& node); + + /** Make a node switch place with its next sibling (if it is not + already the last child). */ + void move_down(const SgfNode& node); + + /** Make a node the root node of the tree. + All nodes that are not the given node or in the subtree below it are + deleted. Note that this operation in general creates a semantically + invalid tree (e.g. missing GM or CA property in the new root). You need + to add those after this function. In general, you will also have to + examine the nodes in the path to the node in the original tree and then + make the tree valid again after calling make_root(). Typically, you + will have to look at the moves played before this node and convert them + into setup properties to add to the new root such that the board + position at this node is the same as originally. */ + void make_root(const SgfNode& node); + + void make_main_variation(const SgfNode& node); + + bool contains(const SgfNode& node) const; + + template + void set_property(const SgfNode& node, const string& id, const T& value); + + void set_property(const SgfNode& node, const string& id, const char* value); + + template + void set_property(const SgfNode& node, const string& id, + const vector& values); + + void set_property_remove_empty(const SgfNode& node, + const string& id, const string& value); + + bool remove_property(const SgfNode& node, const string& id); + + void move_property_to_front(const SgfNode& node, const string& id); + + /** See Node::remove_children() */ + unique_ptr remove_children(const SgfNode& node); + + void append(const SgfNode& node, unique_ptr child); + + /** Get comment. + @return The comment, or an empty string if the node contains no + comment. */ + string get_comment(const SgfNode& node) const; + + void set_comment(const SgfNode& node, const string& s); + + void remove_move_annotation(const SgfNode& node); + + static double get_good_move(const SgfNode& node); + + void set_good_move(const SgfNode& node, double value = 1); + + static double get_bad_move(const SgfNode& node); + + void set_bad_move(const SgfNode& node, double value = 1); + + static bool is_doubtful_move(const SgfNode& node); + + void set_doubtful_move(const SgfNode& node); + + static bool is_interesting_move(const SgfNode& node); + + void set_interesting_move(const SgfNode& node); + + void set_charset(const string& charset); + + void set_application(const string& name, const string& version = ""); + + string get_date() const; + + void set_date(const string& date); + + /** Get today's date in format YYYY-MM-DD as required by DT property. */ + static string get_date_today(); + + void set_date_today(); + + string get_event() const; + + void set_event(const string& event); + + string get_round() const; + + void set_round(const string& date); + + string get_time() const; + + void set_time(const string& time); + + bool has_variations() const; + +private: + bool m_modified; + + unique_ptr m_root; + + SgfNode& non_const(const SgfNode& node); +}; + +inline void SgfTree::append(const SgfNode& node, unique_ptr child) +{ + if (child) + m_modified = true; + non_const(node).append(move(child)); +} + +inline void SgfTree::clear_modified() +{ + m_modified = false; +} + +inline string SgfTree::get_date() const +{ + return m_root->get_property("DT", ""); +} + +inline string SgfTree::get_event() const +{ + return m_root->get_property("EV", ""); +} + +inline bool SgfTree::is_modified() const +{ + return m_modified; +} + +inline string SgfTree::get_round() const +{ + return m_root->get_property("RO", ""); +} + +inline const SgfNode& SgfTree::get_root() const +{ + return *m_root; +} + +inline string SgfTree::get_time() const +{ + return m_root->get_property("TM", ""); +} + +inline SgfNode& SgfTree::non_const(const SgfNode& node) +{ + LIBBOARDGAME_ASSERT(contains(node)); + return const_cast(node); +} + +inline unique_ptr SgfTree::remove_children(const SgfNode& node) +{ + if (node.has_children()) + m_modified = true; + return non_const(node).remove_children(); +} + +inline void SgfTree::set_charset(const string& charset) +{ + set_property_remove_empty(get_root(), "CA", charset); +} + +inline void SgfTree::set_date(const string& date) +{ + set_property_remove_empty(get_root(), "DT", date); +} + +inline void SgfTree::set_event(const string& event) +{ + set_property_remove_empty(get_root(), "EV", event); +} + +inline void SgfTree::set_modified() +{ + m_modified = true; +} + +template +void SgfTree::set_property(const SgfNode& node, const string& id, const T& value) +{ + bool was_changed = non_const(node).set_property(id, value); + if (was_changed) + m_modified = true; +} + +template +void SgfTree::set_property(const SgfNode& node, const string& id, + const vector& values) +{ + bool was_changed = non_const(node).set_property(id, values); + if (was_changed) + m_modified = true; +} + +inline void SgfTree::set_round(const string& round) +{ + set_property_remove_empty(get_root(), "RO", round); +} + +inline void SgfTree::set_time(const string& time) +{ + set_property_remove_empty(get_root(), "TM", time); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_SGF_TREE_H diff --git a/src/libboardgame_sgf/SgfUtil.cpp b/src/libboardgame_sgf/SgfUtil.cpp new file mode 100644 index 0000000..973ef5e --- /dev/null +++ b/src/libboardgame_sgf/SgfUtil.cpp @@ -0,0 +1,221 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SgfUtil.h" + +#include +#include +#include "InvalidPropertyValue.h" +#include "TreeWriter.h" +#include "libboardgame_util/StringUtil.h" + +namespace libboardgame_sgf { +namespace util { + +using libboardgame_util::get_letter_coord; + +//----------------------------------------------------------------------------- + +const SgfNode& back_to_main_variation(const SgfNode& node) +{ + if (is_main_variation(node)) + return node; + auto current = &node; + while (! is_main_variation(*current)) + current = ¤t->get_parent(); + return current->get_first_child(); +} + +const SgfNode& beginning_of_branch(const SgfNode& node) +{ + auto current = node.get_parent_or_null(); + if (! current) + return node; + while (true) + { + auto parent = current->get_parent_or_null(); + if (! parent || ! parent->has_single_child()) + break; + current = parent; + } + return *current; +} + +const SgfNode* find_next_comment(const SgfNode& node) +{ + auto current = get_next_node(node); + while (current) + { + if (has_comment(*current)) + return current; + current = get_next_node(*current); + } + return nullptr; +} + +const SgfNode& find_root(const SgfNode& node) +{ + auto current = &node; + while (current->has_parent()) + current = ¤t->get_parent(); + return *current; +} + +const SgfNode& get_last_node(const SgfNode& node) +{ + auto n = &node; + while (n->has_children()) + n = &n->get_first_child(); + return *n; +} + +unsigned get_depth(const SgfNode& node) +{ + unsigned depth = 0; + auto current = &node; + while (current->has_parent()) + { + current = ¤t->get_parent(); + ++depth; + } + return depth; +} + +const char* get_move_annotation(const SgfTree& tree, const SgfNode& node) +{ + double goodMove = tree.get_good_move(node); + if (goodMove > 1) + return "!!"; + if (goodMove > 0) + return "!"; + double badMove = tree.get_bad_move(node); + if (badMove > 1) + return "??"; + if (badMove > 0) + return "?"; + if (tree.is_interesting_move(node)) + return "!?"; + if (tree.is_doubtful_move(node)) + return "?!"; + return ""; +} + +const SgfNode* get_next_earlier_variation(const SgfNode& node) +{ + auto child = &node; + auto current = node.get_parent_or_null(); + while (current && ! child->get_sibling()) + { + child = current; + current = current->get_parent_or_null(); + } + if (! current) + return nullptr; + return child->get_sibling(); +} + +const SgfNode* get_next_node(const SgfNode& node) +{ + auto child = node.get_first_child_or_null(); + if (child) + return child; + return get_next_earlier_variation(node); +} + +void get_path_from_root(const SgfNode& node, vector& path) +{ + auto current = &node; + path.assign(1, current); + while(current->has_parent()) + { + current = ¤t->get_parent(); + path.push_back(current); + } + reverse(path.begin(), path.end()); +} + +string get_variation_string(const SgfNode& node) +{ + string result; + auto current = &node; + unsigned depth = get_depth(*current); + while (current->has_parent()) + { + auto& parent = current->get_parent(); + if (parent.get_nu_children() > 1) + { + unsigned index = parent.get_child_index(*current); + if (index > 0) + { + ostringstream s; + s << depth << get_letter_coord(index); + if (! result.empty()) + s << '-' << result; + result = s.str(); + } + } + current = &parent; + --depth; + } + return result; +} + +bool has_comment(const SgfNode& node) +{ + return node.has_property("C"); +} + +bool has_earlier_variation(const SgfNode& node) +{ + auto current = node.get_parent_or_null(); + if (! current) + return false; + while (true) + { + auto parent = current->get_parent_or_null(); + if (! parent) + return false; + if (! parent->has_single_child()) + return true; + current = parent; + } +} + +bool is_empty(const SgfTree& tree) +{ + auto& root = tree.get_root(); + if (root.has_children()) + return false; + for (auto& p : root.get_properties()) + { + auto& id = p.id; + if (id != "GM" && id != "CA" && id != "AP" && id != "DT") + return false; + } + return true; +} + +bool is_main_variation(const SgfNode& node) +{ + auto current = &node; + while (current->has_parent()) + { + auto& parent = current->get_parent(); + if (current != &parent.get_first_child()) + return false; + current = &parent; + } + return true; +} + +//----------------------------------------------------------------------------- + +} // namespace util +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/SgfUtil.h b/src/libboardgame_sgf/SgfUtil.h new file mode 100644 index 0000000..d06c5f8 --- /dev/null +++ b/src/libboardgame_sgf/SgfUtil.h @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_SGF_UTIL_H +#define LIBBOARDGAME_SGF_SGF_UTIL_H + +#include +#include "SgfTree.h" + +namespace libboardgame_sgf { +namespace util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Return the last node in the current variation that had a sibling. */ +const SgfNode& beginning_of_branch(const SgfNode& node); + +/** Find next node with a comment in the iteration through complete tree. + @param node The current node in the iteration. + @return The next node in the iteration through the complete tree + after the current node that has a comment. */ +const SgfNode* find_next_comment(const SgfNode& node); + +const SgfNode& find_root(const SgfNode& node); + +/** Get the depth of a node. + The root node has depth 0. */ +unsigned get_depth(const SgfNode& node); + +/** Get list of nodes from root to a target node. + @param node The target node. + @param[out] path The list of nodes. */ +void get_path_from_root(const SgfNode& node, vector& path); + +const SgfNode& get_last_node(const SgfNode& node); + +/** Get a string representation of move annotation properties. */ +const char* get_move_annotation(const SgfTree& tree, const SgfNode& node); + +/** Get next node for iteration through complete tree. */ +const SgfNode* get_next_node(const SgfNode& node); + +/** Return next variation before this node. */ +const SgfNode* get_next_earlier_variation(const SgfNode& node); + +/** Get a text representation of the variation of a certain node. + The variation string is a sequence of X.Y for each branching into a + variation that is not the first child since the root node separated by + commas, with X being the depth of the child node (starting at 0, and + therefore equivalent to the move number if there are no non-root nodes + without moves) and Y being the number of the child (starting at 1). */ +string get_variation_string(const SgfNode& node); + +/** Check if any previous node had a sibling. */ +bool has_earlier_variation(const SgfNode& node); + +bool is_main_variation(const SgfNode& node); + +const SgfNode& back_to_main_variation(const SgfNode& node); + +bool has_comment(const SgfNode& node); + +/** Check if a tree doesn't contain nodes apart from the root node + or properties apart from some trivial properties (GM, CA, AP or DT) */ +bool is_empty(const SgfTree& tree); + +//----------------------------------------------------------------------------- + +} // namespace util +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_SGF_UTIL_H diff --git a/src/libboardgame_sgf/TreeReader.cpp b/src/libboardgame_sgf/TreeReader.cpp new file mode 100644 index 0000000..31553b7 --- /dev/null +++ b/src/libboardgame_sgf/TreeReader.cpp @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/TreeReader.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TreeReader.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +TreeReader::TreeReader() = default; + +TreeReader::~TreeReader() = default; + +unique_ptr TreeReader::get_tree_transfer_ownership() +{ + return move(m_root); +} + +void TreeReader::on_begin_tree(bool is_root) +{ + if (! is_root) + m_stack.push(m_current); +} + +void TreeReader::on_end_tree(bool is_root) +{ + if (! is_root) + { + LIBBOARDGAME_ASSERT(! m_stack.empty()); + m_current = m_stack.top(); + m_stack.pop(); + } +} + +void TreeReader::on_begin_node(bool is_root) +{ + if (is_root) + { + m_root.reset(new SgfNode); + m_current = m_root.get(); + } + else + m_current = &m_current->create_new_child(); +} + +void TreeReader::on_end_node() +{ +} + +void TreeReader::on_property(const string& identifier, + const vector& values) +{ + m_current->set_property(identifier, values); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/TreeReader.h b/src/libboardgame_sgf/TreeReader.h new file mode 100644 index 0000000..e79a994 --- /dev/null +++ b/src/libboardgame_sgf/TreeReader.h @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/TreeReader.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_TREE_READER_H +#define LIBBOARDGAME_SGF_TREE_READER_H + +#include +#include +#include "Reader.h" +#include "SgfNode.h" + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +class TreeReader + : public Reader +{ +public: + TreeReader(); + + ~TreeReader(); + + void on_begin_tree(bool is_root) override; + + void on_end_tree(bool is_root) override; + + void on_begin_node(bool is_root) override; + + void on_end_node() override; + + void on_property(const string& identifier, + const vector& values) override; + + const SgfNode& get_tree() const; + + /** Get the tree and transfer the ownership to the caller. */ + unique_ptr get_tree_transfer_ownership(); + +private: + SgfNode* m_current = nullptr; + + unique_ptr m_root; + + stack m_stack; +}; + +inline const SgfNode& TreeReader::get_tree() const +{ + return *m_root.get(); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_TREE_READER_H diff --git a/src/libboardgame_sgf/TreeWriter.cpp b/src/libboardgame_sgf/TreeWriter.cpp new file mode 100644 index 0000000..48fc589 --- /dev/null +++ b/src/libboardgame_sgf/TreeWriter.cpp @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/TreeWriter.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TreeWriter.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +TreeWriter::TreeWriter(ostream& out, const SgfNode& root) + : m_root(root), + m_writer(out) +{ +} + +TreeWriter::~TreeWriter() +{ +} + +void TreeWriter::write() +{ + m_writer.begin_tree(); + write_node(m_root); + m_writer.end_tree(); +} + +void TreeWriter::write_node(const SgfNode& node) +{ + m_writer.begin_node(); + for (auto& i : node.get_properties()) + write_property(i.id, i.values); + m_writer.end_node(); + if (! node.has_children()) + return; + else if (node.has_single_child()) + write_node(node.get_child()); + else + for (auto& i : node.get_children()) + { + m_writer.begin_tree(); + write_node(i); + m_writer.end_tree(); + } +} + +void TreeWriter::write_property(const string& id, const vector& values) +{ + m_writer.write_property(id, values); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/TreeWriter.h b/src/libboardgame_sgf/TreeWriter.h new file mode 100644 index 0000000..3e9af22 --- /dev/null +++ b/src/libboardgame_sgf/TreeWriter.h @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/TreeWriter.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_TREE_WRITER_H +#define LIBBOARDGAME_SGF_TREE_WRITER_H + +#include "SgfNode.h" +#include "Writer.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +class TreeWriter +{ +public: + TreeWriter(ostream& out, const SgfNode& root); + + virtual ~TreeWriter(); + + /** Overridable function to write a property. + Can be used in subclasses, for example, to replace or remove obsolete + properties or do other sanitizing. */ + virtual void write_property(const string& id, + const vector& values); + + + /** @name Formatting options. + Should be set before starting to write. */ + /** @{ */ + + void set_one_prop_per_line(bool enable); + + void set_one_prop_value_per_line(bool enable); + + void set_indent(int indent); + + /** @} */ // @name + + + void write(); + +private: + const SgfNode& m_root; + + Writer m_writer; + + void write_node(const SgfNode& node); +}; + +inline void TreeWriter::set_one_prop_per_line(bool enable) +{ + m_writer.set_one_prop_per_line(enable); +} + +inline void TreeWriter::set_one_prop_value_per_line(bool enable) +{ + m_writer.set_one_prop_value_per_line(enable); +} + +inline void TreeWriter::set_indent(int indent) +{ + m_writer.set_indent(indent); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_TREE_WRITER_H diff --git a/src/libboardgame_sgf/Writer.cpp b/src/libboardgame_sgf/Writer.cpp new file mode 100644 index 0000000..0efe0b1 --- /dev/null +++ b/src/libboardgame_sgf/Writer.cpp @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/Writer.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Writer.h" + +#include + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +Writer::Writer(ostream& out) + : m_out(out) +{ } + +void Writer::begin_node() +{ + m_is_first_prop = true; + write_indent(); + m_out << ';'; +} + +void Writer::begin_tree() +{ + write_indent(); + m_out << '('; + // Don't indent the first level + if (m_level > 0) + m_current_indent += m_indent; + ++m_level; + if (m_indent >= 0) + m_out << '\n'; +} + +void Writer::end_node() +{ + if (! m_one_prop_per_line && m_indent >= 0) + m_out << '\n'; +} + +void Writer::end_tree() +{ + --m_level; + if (m_level > 0) + m_current_indent -= m_indent; + write_indent(); + m_out << ')'; + if (m_indent >= 0) + m_out << '\n'; +} + +string Writer::get_escaped(const string& s) +{ + ostringstream buffer; + for (char c : s) + { + if (c == ']' || c == '\\') + buffer << '\\' << c; + else if (c == '\t' || c == '\f' || c == '\v') + // Replace whitespace as required by the SGF standard. + buffer << ' '; + else + buffer << c; + } + return buffer.str(); +} + +void Writer::write_indent() +{ + if (m_indent >= 0) + for (int i = 0; i < m_current_indent; ++i) + m_out << ' '; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/Writer.h b/src/libboardgame_sgf/Writer.h new file mode 100644 index 0000000..56c98da --- /dev/null +++ b/src/libboardgame_sgf/Writer.h @@ -0,0 +1,139 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/Writer.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_WRITER_H +#define LIBBOARDGAME_SGF_WRITER_H + +#include +#include +#include +#include "libboardgame_util/StringUtil.h" + +namespace libboardgame_sgf { + +using namespace std; +using libboardgame_util::to_string; + +//----------------------------------------------------------------------------- + +class Writer +{ +public: + explicit Writer(ostream& out); + + /** @name Formatting options. + Should be set before starting to write. */ + /** @{ */ + + void set_one_prop_per_line(bool enable); + + void set_one_prop_value_per_line(bool enable); + + /** @param indent The number of spaces to indent subtrees, -1 means + to not even use newlines. */ + void set_indent(int indent); + + /** @} */ // @name + + + void begin_tree(); + + void end_tree(); + + void begin_node(); + + void end_node(); + + void write_property(const string& id, const char* value); + + template + void write_property(const string& id, const T& value); + + template + void write_property(const string& id, const vector& values); + +private: + ostream& m_out; + + bool m_one_prop_per_line = false; + + bool m_one_prop_value_per_line = false; + + bool m_is_first_prop; + + int m_indent = 0; + + int m_current_indent = 0; + + unsigned m_level = 0; + + + static string get_escaped(const string& s); + + void write_indent(); +}; + +inline void Writer::set_one_prop_per_line(bool enable) +{ + m_one_prop_per_line = enable; +} + +inline void Writer::set_one_prop_value_per_line(bool enable) +{ + m_one_prop_value_per_line = enable; +} + +inline void Writer::set_indent(int indent) +{ + m_indent = indent; +} + +inline void Writer::write_property(const string& id, const char* value) +{ + vector values(1, value); + write_property(id, values); +} + +template +void Writer::write_property(const string& id, const T& value) +{ + vector values(1, value); + write_property(id, values); +} + +template +void Writer::write_property(const string& id, const vector& values) +{ + if (m_one_prop_per_line && ! m_is_first_prop) + { + write_indent(); + m_out << ' '; + } + m_out << id; + bool is_first_value = true; + for (auto& i : values) + { + if (m_one_prop_per_line && m_one_prop_value_per_line + && ! is_first_value && m_indent >= 0) + { + m_out << '\n'; + int indent = static_cast(m_current_indent + 1 + id.size()); + for (int i = 0; i < indent; ++i) + m_out << ' '; + } + m_out << '[' << get_escaped(to_string(i)) << ']'; + is_first_value = false; + } + if (m_one_prop_per_line && m_indent >= 0) + m_out << '\n'; + m_is_first_prop = false; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_WRITER_H diff --git a/src/libboardgame_sys/CMakeLists.txt b/src/libboardgame_sys/CMakeLists.txt new file mode 100644 index 0000000..a8990ce --- /dev/null +++ b/src/libboardgame_sys/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(boardgame_sys STATIC + Compiler.h + CpuTime.h + CpuTime.cpp + Memory.h + Memory.cpp +) diff --git a/src/libboardgame_sys/Compiler.h b/src/libboardgame_sys/Compiler.h new file mode 100644 index 0000000..70cda1b --- /dev/null +++ b/src/libboardgame_sys/Compiler.h @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/Compiler.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SYS_COMPILER_H +#define LIBBOARDGAME_SYS_COMPILER_H + +#include +#include +#ifdef __GNUC__ +#include +#include +#endif + +namespace libboardgame_sys { + +using namespace std; + +//----------------------------------------------------------------------------- + +#ifdef __GNUC__ +#define LIBBOARDGAME_FORCE_INLINE inline __attribute__((always_inline)) +#elif defined _MSC_VER +#define LIBBOARDGAME_FORCE_INLINE inline __forceinline +#else +#define LIBBOARDGAME_FORCE_INLINE inline +#endif + +#ifdef __GNUC__ +#define LIBBOARDGAME_NOINLINE __attribute__((noinline)) +#elif defined _MSC_VER +#define LIBBOARDGAME_NOINLINE __declspec(noinline) +#else +#define LIBBOARDGAME_NOINLINE +#endif + +#if defined __GNUC__ && ! defined __ICC && ! defined __clang__ +#define LIBBOARDGAME_FLATTEN __attribute__((flatten)) +#else +#define LIBBOARDGAME_FLATTEN +#endif + +template +string get_type_name(const T& t) +{ +#ifdef __GNUC__ + int status; + char* name_ptr = abi::__cxa_demangle(typeid(t).name(), nullptr, nullptr, + &status); + if (status == 0) + { + string result(name_ptr); + free(name_ptr); + return result; + } +#endif + return typeid(t).name(); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sys + +#endif // LIBBOARDGAME_SYS_COMPILER_H diff --git a/src/libboardgame_sys/CpuTime.cpp b/src/libboardgame_sys/CpuTime.cpp new file mode 100644 index 0000000..2d9b0bc --- /dev/null +++ b/src/libboardgame_sys/CpuTime.cpp @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/CpuTime.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "CpuTime.h" + +#ifdef _WIN32 +#include +#endif + +#if HAVE_UNISTD_H +#include +#endif + +#if HAVE_SYS_TIMES_H +#include +#endif + +namespace libboardgame_sys { + +//----------------------------------------------------------------------------- + +double cpu_time() +{ +#ifdef _WIN32 + + FILETIME create; + FILETIME exit; + FILETIME sys; + FILETIME user; + if (! GetProcessTimes(GetCurrentProcess(), &create, &exit, &sys, &user)) + return -1; + ULARGE_INTEGER sys_int; + sys_int.LowPart = sys.dwLowDateTime; + sys_int.HighPart = sys.dwHighDateTime; + ULARGE_INTEGER user_int; + user_int.LowPart = user.dwLowDateTime; + user_int.HighPart = user.dwHighDateTime; + return (sys_int.QuadPart + user_int.QuadPart) * 1e-7; + +#elif HAVE_UNISTD_H && HAVE_SYS_TIMES_H + static double ticks_per_second = double(sysconf(_SC_CLK_TCK)); + struct tms buf; + if (times(&buf) == clock_t(-1)) + return -1; + clock_t clock_ticks = + buf.tms_utime + buf.tms_stime + buf.tms_cutime + buf.tms_cstime; + return double(clock_ticks) / ticks_per_second; + +#else + + return -1; + +#endif +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sys diff --git a/src/libboardgame_sys/CpuTime.h b/src/libboardgame_sys/CpuTime.h new file mode 100644 index 0000000..45b0f13 --- /dev/null +++ b/src/libboardgame_sys/CpuTime.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/CpuTime.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SYS_CPU_TIME_H +#define LIBBOARDGAME_SYS_CPU_TIME_H + +namespace libboardgame_sys { + +//----------------------------------------------------------------------------- + +/** Return the CPU time of the current process. + @return The CPU time of the current process in seconds or -1, if the + CPU time cannot be determined. */ +double cpu_time(); + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sys + +#endif // LIBBOARDGAME_SYS_CPU_TIME_H diff --git a/src/libboardgame_sys/Memory.cpp b/src/libboardgame_sys/Memory.cpp new file mode 100644 index 0000000..31fe4a7 --- /dev/null +++ b/src/libboardgame_sys/Memory.cpp @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/Memory.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Memory.h" + +#ifdef _WIN32 +#include +#include +#else +#include +#endif +// sysctl() is unsupported on Linux with x32 ABI (last checked on Ubuntu 14.10) +#if HAVE_SYS_SYSCTL_H && ! (defined __x86_64__ && defined __ILP32__) +#include +#endif + +namespace libboardgame_sys { + +//----------------------------------------------------------------------------- + +size_t get_memory() +{ +#ifdef _WIN32 + + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); + if (! GlobalMemoryStatusEx(&status)) + return 0; + auto total_virtual = static_cast(status.ullTotalVirtual); + auto total_phys = static_cast(status.ullTotalPhys); + return min(total_virtual, total_phys); + +#elif defined _SC_PHYS_PAGES + + long phys_pages = sysconf(_SC_PHYS_PAGES); + if (phys_pages < 0) + return 0; + long page_size = sysconf(_SC_PAGE_SIZE); + if (page_size < 0) + return 0; + return static_cast(phys_pages) * static_cast(page_size); + +#elif defined HW_PHYSMEM // Mac OS X + + unsigned int phys_mem; + size_t len = sizeof(phys_mem); + int name[2] = { CTL_HW, HW_PHYSMEM }; + if (sysctl(name, 2, &phys_mem, &len, nullptr, 0) != 0 + || len != sizeof(phys_mem)) + return 0; + else + return phys_mem; + +#else + + return 0; + +#endif +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sys diff --git a/src/libboardgame_sys/Memory.h b/src/libboardgame_sys/Memory.h new file mode 100644 index 0000000..769aa1f --- /dev/null +++ b/src/libboardgame_sys/Memory.h @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/Memory.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SYS_MEMORY_H +#define LIBBOARDGAME_SYS_MEMORY_H + +#include + +namespace libboardgame_sys { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Get the physical memory available on the system. + @return The memory in bytes or 0 if the memory could not be determined. */ +size_t get_memory(); + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sys + +#endif // LIBBOARDGAME_SYS_MEMORY_H diff --git a/src/libboardgame_test/CMakeLists.txt b/src/libboardgame_test/CMakeLists.txt new file mode 100644 index 0000000..715549e --- /dev/null +++ b/src/libboardgame_test/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(boardgame_test STATIC + Test.h + Test.cpp +) diff --git a/src/libboardgame_test/Test.cpp b/src/libboardgame_test/Test.cpp new file mode 100644 index 0000000..a1fafd4 --- /dev/null +++ b/src/libboardgame_test/Test.cpp @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_test/Test.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Test.h" + +#include +#include +#include "libboardgame_util/Assert.h" +#include "libboardgame_util/Log.h" + +namespace libboardgame_test { + +//----------------------------------------------------------------------------- + +namespace { + +map& get_all_tests() +{ + static map all_tests; + return all_tests; +} + +string get_fail_msg(const char* file, int line, const string& s) +{ + ostringstream msg; + msg << file << ":" << line << ": " << s; + return msg.str(); +} + +} // namespace + +//----------------------------------------------------------------------------- + +TestFail::TestFail(const char* file, int line, const string& s) + : logic_error(get_fail_msg(file, line, s)) +{ +} + +//----------------------------------------------------------------------------- + +void add_test(const string& name, TestFunction function) +{ + auto& all_tests = get_all_tests(); + LIBBOARDGAME_ASSERT(all_tests.find(name) == all_tests.end()); + all_tests.insert(make_pair(name, function)); +} + +bool run_all_tests() +{ + unsigned nu_fail = 0; + LIBBOARDGAME_LOG("Running ", get_all_tests().size(), " tests..."); + for (auto& i : get_all_tests()) + { + try + { + (i.second)(); + } + catch (const TestFail& e) + { + LIBBOARDGAME_LOG(e.what()); + ++nu_fail; + } + } + if (nu_fail == 0) + { + LIBBOARDGAME_LOG("OK"); + return true; + } + else + { + LIBBOARDGAME_LOG(nu_fail, " tests failed.\nFAIL"); + return false; + } +} + +bool run_test(const string& name) +{ + for (auto& i : get_all_tests()) + if (i.first == name) + { + LIBBOARDGAME_LOG("Running ", name, "..."); + try + { + (i.second)(); + LIBBOARDGAME_LOG("OK"); + return true; + } + catch (const TestFail& e) + { + LIBBOARDGAME_LOG(e.what(), "\nFAIL"); + return false; + } + } + LIBBOARDGAME_LOG("Test not found: ", name); + return false; +} + +int test_main(int argc, char* argv[]) +{ + if (argc < 2) + return run_all_tests() ? 0 : 1; + int result = 0; + for (int i = 1; i < argc; ++i) + if (! run_test(argv[i])) + result = 1; + return result; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_test diff --git a/src/libboardgame_test/Test.h b/src/libboardgame_test/Test.h new file mode 100644 index 0000000..4289341 --- /dev/null +++ b/src/libboardgame_test/Test.h @@ -0,0 +1,153 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_test/Test.h + Provides functionality similar to Boost.Test. + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_TEST_TEST_H +#define LIBBOARDGAME_TEST_TEST_H + +#include +#include +#include +#include + +namespace libboardgame_test { + +using namespace std; + +//----------------------------------------------------------------------------- + +typedef void (*TestFunction)(); + +//----------------------------------------------------------------------------- + +class TestFail + : public logic_error +{ +public: + TestFail(const char* file, int line, const string& s); +}; + +//----------------------------------------------------------------------------- + +void add_test(const string& name, TestFunction function); + +bool run_all_tests(); + +bool run_test(const string& name); + +/** Main function that runs all tests (if no arguments) or only the tests + given as arguments. */ +int test_main(int argc, char* argv[]); + +//----------------------------------------------------------------------------- + +/** Helper class that automatically adds a test when an instance is + declared. */ +struct TestRegistrar +{ + TestRegistrar(const string& name, TestFunction function) + { + add_test(name, function); + } +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_test + +//----------------------------------------------------------------------------- + +#define LIBBOARDGAME_TEST_CASE(name) \ + void name(); \ + libboardgame_test::TestRegistrar name##_registrar(#name, name); \ + void name() + + +#define LIBBOARDGAME_CHECK(expr) \ + if (! (expr)) \ + throw libboardgame_test::TestFail(__FILE__, __LINE__, "check failed") + +#define LIBBOARDGAME_CHECK_EQUAL(expr1, expr2) \ + { \ + using libboardgame_test::TestFail; \ + auto result1 = (expr1); \ + auto result2 = (expr2); \ + if (result1 != result2) \ + { \ + ostringstream msg; \ + msg << "'" << result1 << " != " << "'" << result2 << "'"; \ + throw TestFail(__FILE__, __LINE__, msg.str()); \ + } \ + } + +#define LIBBOARDGAME_CHECK_THROW(expr, exception) \ + { \ + using libboardgame_test::TestFail; \ + bool was_thrown = false; \ + try \ + { \ + expr; \ + } \ + catch (const exception&) \ + { \ + was_thrown = true; \ + } \ + if (! was_thrown) \ + { \ + ostringstream msg; \ + msg << "Exception '" << #exception << "' was not thrown"; \ + throw TestFail(__FILE__, __LINE__, msg.str()); \ + } \ + } + +#define LIBBOARDGAME_CHECK_NO_THROW(expr) \ + { \ + using libboardgame_test::TestFail; \ + try \ + { \ + expr; \ + } \ + catch (...) \ + { \ + throw TestFail(__FILE__, __LINE__, \ + "Unexcpected exception was thrown"); \ + } \ + } + +/** Compare floating points using a tolerance in percent. */ +#define LIBBOARDGAME_CHECK_CLOSE(expr1, expr2, tolerance) \ + { \ + using libboardgame_test::TestFail; \ + auto result1 = (expr1); \ + auto result2 = (expr2); \ + if (fabs(result1 - result2) > 0.01 * tolerance * result1) \ + { \ + ostringstream msg; \ + msg << "Difference between " << result1 << " and " \ + << result2 << " exceeds " << (0.01 * tolerance) \ + << " percent"; \ + throw TestFail(__FILE__, __LINE__, msg.str()); \ + } \ + } + +/** Compare floating points using an epsilon. */ +#define LIBBOARDGAME_CHECK_CLOSE_EPS(expr1, expr2, epsilon) \ + { \ + using libboardgame_test::TestFail; \ + auto result1 = (expr1); \ + auto result2 = (expr2); \ + if (fabs(result1 - result2) > epsilon) \ + { \ + ostringstream msg; \ + msg << "Difference between " << result1 << " and " \ + << result2 << " exceeds " << epsilon; \ + throw TestFail(__FILE__, __LINE__, msg.str()); \ + } \ + } + +//----------------------------------------------------------------------------- + +#endif // LIBBOARDGAME_TEST_TEST_H diff --git a/src/libboardgame_test_main/CMakeLists.txt b/src/libboardgame_test_main/CMakeLists.txt new file mode 100644 index 0000000..ed1f426 --- /dev/null +++ b/src/libboardgame_test_main/CMakeLists.txt @@ -0,0 +1 @@ +add_library(boardgame_test_main STATIC Main.cpp) diff --git a/src/libboardgame_test_main/Main.cpp b/src/libboardgame_test_main/Main.cpp new file mode 100644 index 0000000..2b9a586 --- /dev/null +++ b/src/libboardgame_test_main/Main.cpp @@ -0,0 +1,16 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_test_main/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "libboardgame_test/Test.h" + +//----------------------------------------------------------------------------- + +int main(int argc, char* argv[]) +{ + return libboardgame_test::test_main(argc, argv); +} + +//---------------------------------------------------------------------------- diff --git a/src/libboardgame_util/Abort.cpp b/src/libboardgame_util/Abort.cpp new file mode 100644 index 0000000..f4e02bb --- /dev/null +++ b/src/libboardgame_util/Abort.cpp @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Abort.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Abort.h" + +//---------------------------------------------------------------------------- + +namespace libboardgame_util { + +using namespace std; + +atomic abort(false); + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Abort.h b/src/libboardgame_util/Abort.h new file mode 100644 index 0000000..c5c3f94 --- /dev/null +++ b/src/libboardgame_util/Abort.h @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Abort.h + Global flag to interrupt move generation or other commands. + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_ABORT_H +#define LIBBOARDGAME_UTIL_ABORT_H + +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +extern atomic abort; + +inline void clear_abort() +{ + abort.store(false, memory_order_seq_cst); +} + +inline bool get_abort() +{ + return abort.load(memory_order_relaxed); +} + +inline void set_abort() +{ + abort.store(true, memory_order_seq_cst); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_ABORT_H diff --git a/src/libboardgame_util/ArrayList.h b/src/libboardgame_util/ArrayList.h new file mode 100644 index 0000000..d72cb46 --- /dev/null +++ b/src/libboardgame_util/ArrayList.h @@ -0,0 +1,353 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/ArrayList.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_ARRAY_LIST_H +#define LIBBOARDGAME_UTIL_ARRAY_LIST_H + +#include +#include +#include +#include +#include "Assert.h" + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Array-based list with maximum number of elements. + The user is responsible for not inserting more than the maximum number of + elements. The elements must be default-constructible. If the size of the + list shrinks, the destructor of elements will be not be called and the + elements above the current size are still usable with get_unchecked(). + The list contains iterator definitions that are compatible with STL + containers. + @tparam T The type of the elements + @tparam M The maximum number of elements + @tparam I The integer type for the array size */ +template +class ArrayList +{ +public: + typedef I IntType; + + static_assert(numeric_limits::is_integer, ""); + + static const IntType max_size = M; + + typedef typename array::iterator iterator; + + typedef typename array::const_iterator const_iterator; + + typedef T value_type; + + + ArrayList() = default; + + /** Construct list with a single element. */ + explicit ArrayList(const T& t); + + explicit ArrayList(const initializer_list& l); + + /** Assignment operator. + Copies only elements with index below the current size. */ + ArrayList& operator=(const ArrayList& l); + + T& operator[](I i); + + const T& operator[](I i) const; + + /** Get an element whose index may be higher than the current size. */ + T& get_unchecked(I i); + + /** Get an element whose index may be higher than the current size. */ + const T& get_unchecked(I i) const; + + bool operator==(const ArrayList& array_list) const; + + bool operator!=(const ArrayList& array_list) const; + + iterator begin(); + + const_iterator begin() const; + + iterator end(); + + const_iterator end() const; + + T& back(); + + const T& back() const; + + I size() const; + + bool empty() const; + + const T& pop_back(); + + void push_back(const T& t); + + void clear(); + + void assign(const T& t); + + /** Change the size of the list. + Does not call constructors on new elements if the size grows or + destructors of elements if the size shrinks. */ + void resize(I size); + + bool contains(const T& t) const; + + /** Push back element if not already contained in list. + @return @c true if element was not already in list. */ + bool include(const T& t); + + /** Removal of first occurrence of value. + Preserves the order of elements. + @return @c true if value was removed. */ + bool remove(const T& t); + + /** Fast removal of element. + Does not preserve the order of elements. The element will be replaced + with the last element and the list size decremented. */ + void remove_fast(iterator i); + + /** Fast removal of first occurrence of value. + Does not preserve the order of elements. If the value is found, + it will be replaced with the last element and the list size + decremented. + @return @c true if value was removed. */ + bool remove_fast(const T& t); + +private: + array m_a; + + I m_size = 0; +}; + +template +inline ArrayList::ArrayList(const T& t) +{ + assign(t); +} + +template +ArrayList::ArrayList(const initializer_list& l) + : m_size(0) +{ + for (auto& t : l) + push_back(t); +} + +template +auto ArrayList::operator=(const ArrayList& l) -> ArrayList& +{ + m_size = l.size(); + copy(l.begin(), l.end(), begin()); + return *this; +} + +template +inline T& ArrayList::operator[](I i) +{ + LIBBOARDGAME_ASSERT(i < m_size); + return m_a[i]; +} + +template +inline const T& ArrayList::operator[](I i) const +{ + LIBBOARDGAME_ASSERT(i < m_size); + return m_a[i]; +} + +template +bool ArrayList::operator==(const ArrayList& array_list) const +{ + if (m_size != array_list.m_size) + return false; + return equal(begin(), end(), array_list.begin()); +} + +template +bool ArrayList::operator!=(const ArrayList& array_list) const +{ + return ! operator==(array_list); +} + +template +inline void ArrayList::assign(const T& t) +{ + m_size = 1; + m_a[0] = t; +} + +template +inline T& ArrayList::back() +{ + LIBBOARDGAME_ASSERT(m_size > 0); + return m_a[m_size - 1]; +} + +template +inline const T& ArrayList::back() const +{ + LIBBOARDGAME_ASSERT(m_size > 0); + return m_a[m_size - 1]; +} + +template +inline auto ArrayList::begin() -> iterator +{ + return m_a.begin(); +} + +template +inline auto ArrayList::begin() const -> const_iterator +{ + return m_a.begin(); +} + +template +inline void ArrayList::clear() +{ + m_size = 0; +} + +template +bool ArrayList::contains(const T& t) const +{ + return find(begin(), end(), t) != end(); +} + +template +inline bool ArrayList::empty() const +{ + return m_size == 0; +} + +template +inline auto ArrayList::end() -> iterator +{ + return begin() + m_size; +} + +template +inline auto ArrayList::end() const -> const_iterator +{ + return begin() + m_size; +} + +template +inline T& ArrayList::get_unchecked(I i) +{ + LIBBOARDGAME_ASSERT(i < max_size); + return m_a[i]; +} + +template +inline const T& ArrayList::get_unchecked(I i) const +{ + LIBBOARDGAME_ASSERT(i < max_size); + return m_a[i]; +} + +template +bool ArrayList::include(const T& t) +{ + if (contains(t)) + return false; + push_back(t); + return true; +} + +template +inline const T& ArrayList::pop_back() +{ + LIBBOARDGAME_ASSERT(m_size > 0); + return m_a[--m_size]; +} + +template +inline void ArrayList::push_back(const T& t) +{ + LIBBOARDGAME_ASSERT(m_size < max_size); + m_a[m_size++] = t; +} + +template +inline bool ArrayList::remove(const T& t) +{ + auto end = this->end(); + for (auto i = begin(); i != end; ++i) + if (*i == t) + { + --end; + for ( ; i != end; ++i) + *i = *(i + 1); + --m_size; + return true; + } + return false; +} + +template +inline bool ArrayList::remove_fast(const T& t) +{ + auto end = this->end(); + for (auto i = this->begin(); i != end; ++i) + if (*i == t) + { + remove_fast(i); + return true; + } + return false; +} + +template +inline void ArrayList::remove_fast(iterator i) +{ + LIBBOARDGAME_ASSERT(i >= begin()); + LIBBOARDGAME_ASSERT(i < end()); + --m_size; + *i = *(begin() + m_size); +} + +template +inline void ArrayList::resize(I size) +{ + LIBBOARDGAME_ASSERT(size <= max_size); + m_size = size; +} + +template +inline I ArrayList::size() const +{ + return m_size; +} + +//----------------------------------------------------------------------------- + +template +ostream& operator<<(ostream& out, const ArrayList& l) +{ + auto begin = l.begin(); + auto end = l.end(); + if (begin != end) + { + out << *begin; + for (auto i = begin + 1; i != end; ++i) + out << ' ' << *i; + } + return out; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_ARRAY_LIST_H diff --git a/src/libboardgame_util/Assert.cpp b/src/libboardgame_util/Assert.cpp new file mode 100644 index 0000000..91d6617 --- /dev/null +++ b/src/libboardgame_util/Assert.cpp @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Assert.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Assert.h" + +#include + +#if LIBBOARDGAME_DEBUG +#include +#include +#include +#include +#include +#include "Log.h" +#endif + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +namespace { + +list& get_all_handlers() +{ + static list all_handlers; + return all_handlers; +} + +} // namespace + +//---------------------------------------------------------------------------- + +AssertionHandler::AssertionHandler() +{ + get_all_handlers().push_back(this); +} + +AssertionHandler::~AssertionHandler() +{ + get_all_handlers().remove(this); +} + +//---------------------------------------------------------------------------- + +#if LIBBOARDGAME_DEBUG + +void handle_assertion(const char* expression, const char* file, int line) +{ + static bool is_during_handle_assertion = false; + LIBBOARDGAME_LOG(file, ":", line, ": Assertion '", expression, "' failed"); + flush_log(); + if (! is_during_handle_assertion) + { + is_during_handle_assertion = true; + for_each(get_all_handlers().begin(), get_all_handlers().end(), + mem_fun(&AssertionHandler::run)); + } + abort(); +} + +#endif + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Assert.h b/src/libboardgame_util/Assert.h new file mode 100644 index 0000000..cd696fe --- /dev/null +++ b/src/libboardgame_util/Assert.h @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Assert.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_ASSERT_H +#define LIBBOARDGAME_UTIL_ASSERT_H + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +class AssertionHandler +{ +public: + /** Construct and register assertion handler. */ + AssertionHandler(); + + /** Destruct and unregister assertion handler. */ + virtual ~AssertionHandler(); + + virtual void run() = 0; +}; + +#if LIBBOARDGAME_DEBUG + +/** Function used by the LIBBOARDGAME_ASSERT macro to run all assertion + handlers. */ +[[noreturn]] void handle_assertion(const char* expression, const char* file, + int line); + +#endif + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +//----------------------------------------------------------------------------- + +/** @def LIBBOARDGAME_ASSERT + Enhanced assert macro. + This macro is similar to the assert macro in the standard library, but it + allows the user to register assertion handlers that are executed before the + program is aborted. Assertions are only enabled if the macro + LIBBOARDGAME_DEBUG is true. */ +#if LIBBOARDGAME_DEBUG +#define LIBBOARDGAME_ASSERT(expr) \ + ((expr) ? (static_cast(0)) \ + : libboardgame_util::handle_assertion(#expr, __FILE__, __LINE__)) +#else +#define LIBBOARDGAME_ASSERT(expr) (static_cast(0)) +#endif + +//----------------------------------------------------------------------------- + +#endif // LIBBOARDGAME_UTIL_ASSERT_H diff --git a/src/libboardgame_util/Barrier.cpp b/src/libboardgame_util/Barrier.cpp new file mode 100644 index 0000000..cd4a76f --- /dev/null +++ b/src/libboardgame_util/Barrier.cpp @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Barrier.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Barrier.h" + +#include "Assert.h" + +namespace libboardgame_util { + +//---------------------------------------------------------------------------- + +Barrier::Barrier(unsigned count) + : m_threshold(count), + m_count(count) +{ + LIBBOARDGAME_ASSERT(count > 0); +} + +void Barrier::wait() +{ + unique_lock lock(m_mutex); + unsigned current = m_current; + if (--m_count == 0) + { + ++m_current; + m_count = m_threshold; + m_condition.notify_all(); + } + else + while (current == m_current) + m_condition.wait(lock); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Barrier.h b/src/libboardgame_util/Barrier.h new file mode 100644 index 0000000..f96cef8 --- /dev/null +++ b/src/libboardgame_util/Barrier.h @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Barrier.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_BARRIER_H +#define LIBBOARDGAME_UTIL_BARRIER_H + +#include +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Similar to boost::barrier, which does not exist in C++11 */ +class Barrier +{ +public: + explicit Barrier(unsigned count); + + void wait(); + +private: + mutex m_mutex; + + condition_variable m_condition; + + unsigned m_threshold; + + unsigned m_count; + + unsigned m_current = 0; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_BARRIER_H diff --git a/src/libboardgame_util/CMakeLists.txt b/src/libboardgame_util/CMakeLists.txt new file mode 100644 index 0000000..076e97d --- /dev/null +++ b/src/libboardgame_util/CMakeLists.txt @@ -0,0 +1,34 @@ +add_library(boardgame_util STATIC + Abort.h + Abort.cpp + ArrayList.h + Assert.h + Assert.cpp + Barrier.h + Barrier.cpp + CpuTimeSource.h + CpuTimeSource.cpp + FmtSaver.h + IntervalChecker.h + IntervalChecker.cpp + Log.h + Log.cpp + MathUtil.h + Options.h + Options.cpp + RandomGenerator.h + RandomGenerator.cpp + Range.h + Statistics.h + StringUtil.h + StringUtil.cpp + TimeIntervalChecker.h + TimeIntervalChecker.cpp + Timer.h + Timer.cpp + TimeSource.h + TimeSource.cpp + Unused.h + WallTimeSource.h + WallTimeSource.cpp +) diff --git a/src/libboardgame_util/CpuTimeSource.cpp b/src/libboardgame_util/CpuTimeSource.cpp new file mode 100644 index 0000000..e38be00 --- /dev/null +++ b/src/libboardgame_util/CpuTimeSource.cpp @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/CpuTimeSource.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "CpuTimeSource.h" + +#include "libboardgame_sys/CpuTime.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +double CpuTimeSource::operator()() +{ + return libboardgame_sys::cpu_time(); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/CpuTimeSource.h b/src/libboardgame_util/CpuTimeSource.h new file mode 100644 index 0000000..b4ad8de --- /dev/null +++ b/src/libboardgame_util/CpuTimeSource.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/CpuTimeSource.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_CPU_TIME_SOURCE_H +#define LIBBOARDGAME_UTIL_CPU_TIME_SOURCE_H + +#include "TimeSource.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +/** CPU time. + @ref libboardgame_doc_threadsafe_after_construction */ +class CpuTimeSource + : public TimeSource +{ +public: + double operator()() override; +}; +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_CPU_TIME_SOURCE_H diff --git a/src/libboardgame_util/FmtSaver.h b/src/libboardgame_util/FmtSaver.h new file mode 100644 index 0000000..a832e0f --- /dev/null +++ b/src/libboardgame_util/FmtSaver.h @@ -0,0 +1,44 @@ +//---------------------------------------------------------------------------- +/** @file libboardgame_util/FmtSaver.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//---------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_FMT_SAVER_H +#define LIBBOARDGAME_UTIL_FMT_SAVER_H + +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Saves the formatting state of a stream and restores it in its + destructor. */ +class FmtSaver +{ +public: + explicit FmtSaver(ostream& out) + : m_out(out) + { + m_dummy.copyfmt(out); + } + + ~FmtSaver() + { + m_out.copyfmt(m_dummy); + } + +private: + ostream& m_out; + + ios m_dummy{nullptr}; +}; + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_FMT_SAVER_H diff --git a/src/libboardgame_util/IntervalChecker.cpp b/src/libboardgame_util/IntervalChecker.cpp new file mode 100644 index 0000000..520bbdf --- /dev/null +++ b/src/libboardgame_util/IntervalChecker.cpp @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/IntervalChecker.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "IntervalChecker.h" + +#include +#include "Assert.h" +#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG +#include "Log.h" +#endif + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG +#define LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG 0 +#endif + +//----------------------------------------------------------------------------- + +IntervalChecker::IntervalChecker(TimeSource& time_source, double time_interval, + function f) + : m_time_source(time_source), + m_time_interval(time_interval), + m_function(move(f)) +{ +#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG + log(format("IntervalChecker::IntervalChecker: time_interval=%1%") + % time_interval); +#endif + LIBBOARDGAME_ASSERT(time_interval > 0); +} + +bool IntervalChecker::check_expensive() +{ + if (m_result) + return true; + if (m_is_deterministic) + { + m_result = m_function(); + m_count = m_count_interval; + return m_result; + } + double time = m_time_source(); + if (! m_is_first_check) + { + + double diff = time - m_last_time; + double adjust_factor; + if (diff == 0) + adjust_factor = 10; + else + { + adjust_factor = m_time_interval / diff; + if (adjust_factor > 10) + adjust_factor = 10; + else if (adjust_factor < 0.1) + adjust_factor = 0.1; + } + double new_count_interval = adjust_factor * double(m_count_interval); + if (new_count_interval > double(numeric_limits::max())) + m_count_interval = numeric_limits::max(); + else if (new_count_interval < 1) + m_count_interval = 1; + else + m_count_interval = (unsigned)(new_count_interval); + m_result = m_function(); +#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG + log(format("IntervalChecker::check_expensive: " + "diff=%1% adjust_factor=%2% count_interval=%3%") + % diff % adjust_factor % m_count_interval); +#endif + } + else + { +#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG + log("IntervalChecker::check_expensive: is_first_check"); +#endif + m_is_first_check = false; + } + m_last_time = time; + m_count = m_count_interval; + return m_result; +} + +void IntervalChecker::set_deterministic(unsigned interval) +{ + LIBBOARDGAME_ASSERT(interval >= 1); + m_is_deterministic = true; + m_count = interval; + m_count_interval = interval; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/IntervalChecker.h b/src/libboardgame_util/IntervalChecker.h new file mode 100644 index 0000000..fcef4e1 --- /dev/null +++ b/src/libboardgame_util/IntervalChecker.h @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/IntervalChecker.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_INTERVAL_CHECKER_H +#define LIBBOARDGAME_UTIL_INTERVAL_CHECKER_H + +#include +#include "libboardgame_util/TimeSource.h" + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Reduces regular calls to an expensive function to a given time interval. + The class assumes that its check() function is called in regular time + intervals and forwards only every n'th call to the expensive function with + n being adjusted dynamically to a given time interval. check() returns + true, if the expensive function was called and returned true in the + past. */ +class IntervalChecker +{ +public: + /** Constructor. + @param time_source (@ref libboardgame_doc_storesref) + @param time_interval The time interval in seconds + @param f The expensive function */ + IntervalChecker(TimeSource& time_source, double time_interval, + function f); + + bool operator()(); + + /** Disable the dynamic updating of the interval. + Can be used if the non-reproducability of the time measurement used + for dynamic updating of the check interval is undesirable. + @param interval The fixed interval (number of calls) to use for calling + the expensive function. (Must be greater zero). */ + void set_deterministic(unsigned interval); + +protected: + TimeSource& m_time_source; + +private: + bool m_is_first_check = true; + + bool m_is_deterministic = false; + + bool m_result = false; + + unsigned m_count = 1; + + unsigned m_count_interval = 1; + + double m_time_interval; + + double m_last_time; + + function m_function; + + bool check_expensive(); +}; + +inline bool IntervalChecker::operator()() +{ + if (--m_count == 0) + return check_expensive(); + else + return m_result; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_INTERVAL_CHECKER_H diff --git a/src/libboardgame_util/Log.cpp b/src/libboardgame_util/Log.cpp new file mode 100644 index 0000000..c73a0a7 --- /dev/null +++ b/src/libboardgame_util/Log.cpp @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Log.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#if ! LIBBOARDGAME_DISABLE_LOG + +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Log.h" + +#include + +#if defined ANDROID || defined __ANDROID__ +#include +#endif + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +namespace { + +#if defined ANDROID || defined __ANDROID__ + +class AndroidBuf + : public streambuf +{ +public: + AndroidBuf(); + +protected: + int_type overflow(int_type c) override; + + int sync() override; + +private: + static const unsigned buffer_size = 8192; + + char m_buffer[buffer_size]; +}; + +AndroidBuf::AndroidBuf() +{ + setp(m_buffer, m_buffer + buffer_size - 1); +} + +auto AndroidBuf::overflow(int_type c) -> int_type +{ + if (c == traits_type::eof()) + { + *pptr() = traits_type::to_char_type(c); + sbumpc(); + } + return sync() ? traits_type::eof(): traits_type::not_eof(c); +} + +int AndroidBuf::sync() +{ + int n = 0; + if (pbase() != pptr()) + { + __android_log_print(ANDROID_LOG_INFO, "Native", "%s", + string(pbase(), pptr() - pbase()).c_str()); + n = 0; + setp(m_buffer, m_buffer + buffer_size - 1); + } + return n; +} + +AndroidBuf android_buffer; + +#endif // defined(ANDROID) || defined(__ANDROID__) + +} // namespace + +//----------------------------------------------------------------------------- + +ostream* _log_stream = &cerr; + +//----------------------------------------------------------------------------- + +void _log(const string& s) +{ + if (! _log_stream) + return; + if (s.empty()) + *_log_stream << '\n'; + else if (s.back() == '\n') + *_log_stream << s; + else + { + string line = s; + line += '\n'; + *_log_stream << line; + } +} + +void _log_close() +{ +#if defined ANDROID || defined __ANDROID__ + cerr.rdbuf(nullptr); +#endif +} + +void _log_init() +{ +#if defined ANDROID || defined __ANDROID__ + cerr.rdbuf(&android_buffer); +#endif +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +//----------------------------------------------------------------------------- + +#endif // ! LIBBOARDGAME_DISABLE_LOG diff --git a/src/libboardgame_util/Log.h b/src/libboardgame_util/Log.h new file mode 100644 index 0000000..617fd32 --- /dev/null +++ b/src/libboardgame_util/Log.h @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Log.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_LOG_H +#define LIBBOARDGAME_UTIL_LOG_H + +#include +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +#if ! LIBBOARDGAME_DISABLE_LOG +extern ostream* _log_stream; +#endif + +inline void disable_logging() +{ +#if ! LIBBOARDGAME_DISABLE_LOG + _log_stream = nullptr; +#endif +} + +inline ostream* get_log_stream() +{ +#if ! LIBBOARDGAME_DISABLE_LOG + return _log_stream; +#else + return nullptr; +#endif +} + +inline void flush_log() +{ +#if ! LIBBOARDGAME_DISABLE_LOG + if (_log_stream) + _log_stream->flush(); +#endif +} + +//----------------------------------------------------------------------------- + +#if ! LIBBOARDGAME_DISABLE_LOG + +/** Initializes the logging functionality. + This is necessary to call on some platforms at the start of the program + before any calls to log(). + @see LogInitializer */ +void _log_init(); + +/** Closes the logging functionality. + This is necessary to call on some platforms before the program exits. + @see LogInitializer */ +void _log_close(); + +/** Helper function needed for log(const Ts&...) */ +template +void _log_buffered(ostream& buffer, const T& t) +{ + buffer << t; +} + +/** Helper function needed for log(const Ts&...) */ +template +void _log_buffered(ostream& buffer, const T& first, const Ts&... rest) +{ + buffer << first; + _log_buffered(buffer, rest...); +} + +/** Write a string to the log stream. + Appends a newline if the output has no newline at the end. */ +void _log(const string& s); + +/** Write a number of arguments to the log stream. + Writes to a buffer first so there is only a single write to the log + stream. Appends a newline if the output has no newline at the end. */ +template +void _log(const Ts&... args) +{ + if (! _log_stream) + return; + ostringstream buffer; + _log_buffered(buffer, args...); + _log(buffer.str()); +} + +#endif // ! LIBBOARDGAME_DISABLE_LOG + +//----------------------------------------------------------------------------- + +class LogInitializer +{ +public: + LogInitializer() + { +#if ! LIBBOARDGAME_DISABLE_LOG + _log_init(); +#endif + } + + ~LogInitializer() + { +#if ! LIBBOARDGAME_DISABLE_LOG + _log_close(); +#endif + } +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +//----------------------------------------------------------------------------- + +#if ! LIBBOARDGAME_DISABLE_LOG +#define LIBBOARDGAME_LOG(...) libboardgame_util::_log(__VA_ARGS__) +#else +#define LIBBOARDGAME_LOG(...) (static_cast(0)) +#endif + +//----------------------------------------------------------------------------- + +#endif // LIBBOARDGAME_UTIL_LOG_H diff --git a/src/libboardgame_util/MathUtil.h b/src/libboardgame_util/MathUtil.h new file mode 100644 index 0000000..4342c0a --- /dev/null +++ b/src/libboardgame_util/MathUtil.h @@ -0,0 +1,37 @@ +//---------------------------------------------------------------------------- +/** @file libboardgame_util/MathUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//---------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_MATH_UTIL_H +#define LIBBOARDGAME_UTIL_MATH_UTIL_H + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Fast approximation of exp(x). + The error is less than 15% for abs(x) \< 10 */ +template +inline T fast_exp(T x) +{ + x = static_cast(1) + x / static_cast(256); + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + return x; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_MATH_UTIL_H diff --git a/src/libboardgame_util/Options.cpp b/src/libboardgame_util/Options.cpp new file mode 100644 index 0000000..4f1a3f9 --- /dev/null +++ b/src/libboardgame_util/Options.cpp @@ -0,0 +1,161 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Options.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Options.h" + +namespace libboardgame_util { + +//---------------------------------------------------------------------------- + +Options::Options(int argc, const char** argv, const vector& specs) +{ + for (auto& s : specs) + { + auto pos = s.find("|"); + if (pos == string::npos) + pos = s.find(":"); + if (pos != string::npos) + m_names.insert(s.substr(0, pos)); + else + m_names.insert(s); + } + + bool end_of_options = false; + for (int n = 1; n < argc; ++n) + { + const string arg = argv[n]; + if (! end_of_options && arg.find("-") == 0 && arg != "-") + { + if (arg == "--") + { + end_of_options = true; + continue; + } + string name; + string value; + bool needs_arg = false; + if (arg.find("--") == 0) + { + // Long option + name = arg.substr(2); + auto sz = name.size(); + bool found = false; + for (auto& spec : specs) + if (spec.find(name) == 0 + && (spec.size() == sz || spec[sz] == '|' + || spec[sz] == ':' )) + { + found = true; + needs_arg = (! spec.empty() && spec.back() == ':'); + break; + } + if (! found) + throw OptionError("Unknown option " + arg); + } + else + { + // Short options + for (unsigned i = 1; i < arg.size(); ++i) + { + auto c = arg[i]; + bool found = false; + for (auto& spec : specs) + { + auto pos = spec.find("|" + string(1, c)); + if (pos != string::npos) + { + name = spec.substr(0, pos); + found = true; + if (! spec.empty() && spec.back() == ':') + { + // If not last option, no space was used to + // append the value + if (i != arg.size() - 1) + value = arg.substr(i + 1); + else + needs_arg = true; + } + break; + } + } + if (! found) + throw OptionError("Unknown option -" + string(1, c)); + if (needs_arg || ! value.empty()) + break; + m_map.insert(make_pair(name, "")); + } + } + if (needs_arg) + { + bool value_found = false; + ++n; + if (n < argc) + { + value = argv[n]; + if (value.empty() || value[0] != '-') + value_found = true; + } + if (! value_found) + throw OptionError("Option --" + name + " needs value"); + } + m_map.insert(make_pair(name, value)); + } + else + m_args.push_back(arg); + } +} + +Options::Options(int argc, char** argv, const vector& specs) + : Options(argc, const_cast(argv), specs) +{ +} + +Options::~Options() +{ +} + +void Options::check_name(const string& name) const +{ + if (m_names.count(name) == 0) + throw OptionError("Internal error: invalid option name " + name); +} + +bool Options::contains(const string& name) const +{ + check_name(name); + return m_map.count(name) > 0; +} + +string Options::get(const string& name) const +{ + check_name(name); + auto pos = m_map.find(name); + if (pos == m_map.end()) + throw OptionError("Missing option --" + name); + return pos->second; +} + +string Options::get(const string& name, const string& default_value) const +{ + check_name(name); + auto pos = m_map.find(name); + if (pos == m_map.end()) + return default_value; + return pos->second; +} + +string Options::get(const string& name, const char* default_value) const +{ + return get(name, string(default_value)); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Options.h b/src/libboardgame_util/Options.h new file mode 100644 index 0000000..4f3504e --- /dev/null +++ b/src/libboardgame_util/Options.h @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Options.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_OPTIONS_H +#define LIBBOARDGAME_UTIL_OPTIONS_H + +#include +#include +#include +#include +#include +#include "StringUtil.h" +#include "libboardgame_sys/Compiler.h" + +namespace libboardgame_util { + +using namespace std; +using libboardgame_sys::get_type_name; + +//---------------------------------------------------------------------------- + +class OptionError + : public runtime_error +{ + using runtime_error::runtime_error; +}; + +//---------------------------------------------------------------------------- + +/** Parser for command line options. + The syntax of options is similar to GNU getopt. Options start with "--" + and an option name. Options have optional short (single-character) names + that are used with a single "-" and can be combined if all but the last + option have no value. A single "--" stops option parsing to support + non-option arguments that start with "-". */ +class Options +{ +public: + /** Create options from arguments to main(). + @param argc + @param argv + @param specs A string per option that describes the option. The + description is the long name of the option, followed by and optional + '|' and a character for the short name of the option, followed by an + optional ':' if the option needs a value. + @throws OptionError on error */ + Options(int argc, const char** argv, const vector& specs); + + /** Overloaded version for con-const character strings in argv. + Needed because the portable signature of main is (int, char**). + argv is not modified by this constructor. */ + Options(int argc, char** argv, const vector& specs); + + ~Options(); + + /** Check if an option exists in the command line arguments. + @param name The (long) option name. */ + bool contains(const string& name) const; + + string get(const string& name) const; + + string get(const string& name, const string& default_value) const; + + string get(const string& name, const char* default_value) const; + + /** Get option value. + @param name The (long) option name. + @throws OptionError If option does not exist or has the wrong type. */ + template + T get(const string& name) const; + + /** Get option value or default value. + @param name The (long) option name. + @param default_value A default value. + @return The option value or the default value if the option does not + exist. */ + template + T get(const string& name, const T& default_value) const; + + /** Remaining command line arguments that are not an option or an option + value. */ + const vector& get_args() const; + +private: + set m_names; + + vector m_args; + + map m_map; + + void check_name(const string& name) const; +}; + +template +T Options::get(const string& name) const +{ + T t; + if (! from_string(get(name), t)) + throw OptionError("Option --" + name + " needs type " + + get_type_name(t)); + return t; +} + +template +T Options::get(const string& name, const T& default_value) const +{ + if (! contains(name)) + return default_value; + return get(name); +} + +inline const vector& Options::get_args() const +{ + return m_args; +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_OPTIONS_H diff --git a/src/libboardgame_util/RandomGenerator.cpp b/src/libboardgame_util/RandomGenerator.cpp new file mode 100644 index 0000000..737ca93 --- /dev/null +++ b/src/libboardgame_util/RandomGenerator.cpp @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/RandomGenerator.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RandomGenerator.h" + +#include + +namespace libboardgame_util { + +//---------------------------------------------------------------------------- + +namespace { + +bool is_seed_set = false; + +RandomGenerator::ResultType the_seed; + +list& get_all_generators() +{ + static list all_generators; + return all_generators; +} + +RandomGenerator::ResultType get_nondet_seed() +{ + random_device generator; + return generator(); +} + +} // namespace + +//----------------------------------------------------------------------------- + +RandomGenerator::RandomGenerator() +{ + set_seed(is_seed_set ? the_seed : get_nondet_seed()); + get_all_generators().push_back(this); +} + +RandomGenerator::~RandomGenerator() +{ + get_all_generators().remove(this); +} + +bool RandomGenerator::has_global_seed() +{ + return is_seed_set; +} + +void RandomGenerator::set_global_seed(ResultType seed) +{ + is_seed_set = true; + the_seed = seed; + for (RandomGenerator* i : get_all_generators()) + i->set_seed(the_seed); +} + +void RandomGenerator::set_global_seed_last() +{ + if (is_seed_set) + for (RandomGenerator* i : get_all_generators()) + i->set_seed(the_seed); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/RandomGenerator.h b/src/libboardgame_util/RandomGenerator.h new file mode 100644 index 0000000..4f00966 --- /dev/null +++ b/src/libboardgame_util/RandomGenerator.h @@ -0,0 +1,99 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/RandomGenerator.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_RANDOM_GENERATOR_H +#define LIBBOARDGAME_UTIL_RANDOM_GENERATOR_H + +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Fast pseudo-random number generator. + This is a fast and low-quality pseudo-random number generator for tasks + like opening book move selection or even playouts in Monte-Carlo tree + search (does not seem to be sensitive to the quality of the generator). + All instances of this class register themselves automatically at a + global list of random generators, such that the random seed can be + changed at all existing generators with a single function call. + (@ref libboardgame_doc_threadsafe_after_construction) */ +class RandomGenerator +{ +public: + typedef minstd_rand Generator; + + typedef Generator::result_type ResultType; + + + /** Set seed for all currently existing and future generators. + If this function is never called, a non-deterministic seed is used. */ + static void set_global_seed(ResultType seed); + + /** Set seed to last seed for all currently existing and future + generators. + Sets the seed to the last seed that was set with set_seed(). If no seed + was explicitely defined with set_seed(), then this function does + nothing. */ + static void set_global_seed_last(); + + /** Check if a global seed was set. + User code might want to take more measures if a global seed was set to + become fully deterministic (e.g. avoid decisions based on time + measurements). */ + static bool has_global_seed(); + + + /** Constructor. + Constructs the random generator with the global seed, if one was + defined, otherwise with a non-deterministic seed. */ + RandomGenerator(); + + ~RandomGenerator(); + + void set_seed(ResultType seed); + + ResultType generate(); + + /** Generate a float in [a..b]. */ + float generate_float(float a, float b); + + /** Generate a double in [a..b]. */ + double generate_double(double a, double b); + +private: + Generator m_generator; +}; + +inline RandomGenerator::ResultType RandomGenerator::generate() +{ + return m_generator(); +} + +inline double RandomGenerator::generate_double(double a, double b) +{ + uniform_real_distribution distribution(a, b); + return distribution(m_generator); +} + +inline float RandomGenerator::generate_float(float a, float b) +{ + uniform_real_distribution distribution(a, b); + return distribution(m_generator); +} + +inline void RandomGenerator::set_seed(ResultType seed) +{ + m_generator.seed(seed); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_RANDOM_GENERATOR_H diff --git a/src/libboardgame_util/Range.h b/src/libboardgame_util/Range.h new file mode 100644 index 0000000..b206f34 --- /dev/null +++ b/src/libboardgame_util/Range.h @@ -0,0 +1,52 @@ +//---------------------------------------------------------------------------- +/** @file libboardgame_util/Range.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//---------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_RANGE_H +#define LIBBOARDGAME_UTIL_RANGE_H + +#include + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +template +class Range +{ +public: + Range(T* begin, T* end) + : m_begin(begin), + m_end(end) + { } + + T* begin() const { return m_begin; } + + T* end() const { return m_end; } + + size_t size() const { return m_end - m_begin; } + + bool contains(T& t) const; + +private: + T* m_begin; + + T* m_end; +}; + +template +bool Range::contains(T& t) const +{ + for (auto& i : *this) + if (i == t) + return true; + return false; +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_RANGE_H diff --git a/src/libboardgame_util/Statistics.h b/src/libboardgame_util/Statistics.h new file mode 100644 index 0000000..02e4b6b --- /dev/null +++ b/src/libboardgame_util/Statistics.h @@ -0,0 +1,457 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Statistics.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_STATISTICS_H +#define LIBBOARDGAME_UTIL_STATISTICS_H + +#include +#include +#include +#include +#include +#include +#include +#include "FmtSaver.h" + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +template +class StatisticsBase +{ +public: + /** Constructor. + @param init_val The value to return in get_mean() if count is 0. This + value does not affect the mean returned if count is greater 0. */ + explicit StatisticsBase(FLOAT init_val = 0); + + void add(FLOAT val); + + void clear(FLOAT init_val = 0); + + FLOAT get_count() const; + + FLOAT get_mean() const; + + void write(ostream& out, bool fixed = false, + unsigned precision = 6) const; + +private: + FLOAT m_count; + + FLOAT m_mean; +}; + +template +inline StatisticsBase::StatisticsBase(FLOAT init_val) +{ + clear(init_val); +} + +template +void StatisticsBase::add(FLOAT val) +{ + FLOAT count = m_count; + ++count; + val -= m_mean; + m_mean += val / count; + m_count = count; +} + +template +inline void StatisticsBase::clear(FLOAT init_val) +{ + m_count = 0; + m_mean = init_val; +} + +template +inline FLOAT StatisticsBase::get_count() const +{ + return m_count; +} + +template +inline FLOAT StatisticsBase::get_mean() const +{ + return m_mean; +} + +template +void StatisticsBase::write(ostream& out, bool fixed, + unsigned precision) const +{ + FmtSaver saver(out); + if (fixed) + out << std::fixed; + out << setprecision(precision) << m_mean; +} + +//---------------------------------------------------------------------------- + +template +class Statistics +{ +public: + explicit Statistics(FLOAT init_val = 0); + + void add(FLOAT val); + + void clear(FLOAT init_val = 0); + + FLOAT get_mean() const; + + FLOAT get_count() const; + + FLOAT get_deviation() const; + + FLOAT get_error() const; + + FLOAT get_variance() const; + + void write(ostream& out, bool fixed = false, + unsigned precision = 6) const; + +private: + StatisticsBase m_statistics_base; + + FLOAT m_variance; +}; + +template +inline Statistics::Statistics(FLOAT init_val) +{ + clear(init_val); +} + +template +void Statistics::add(FLOAT val) +{ + if (get_count() > 0) + { + FLOAT count_old = get_count(); + FLOAT mean_old = get_mean(); + m_statistics_base.add(val); + FLOAT mean = get_mean(); + FLOAT count = get_count(); + m_variance = (count_old * (m_variance + mean_old * mean_old) + + val * val) / count - mean * mean; + } + else + { + m_statistics_base.add(val); + m_variance = 0; + } +} + +template +inline void Statistics::clear(FLOAT init_val) +{ + m_statistics_base.clear(init_val); + m_variance = 0; +} + +template +inline FLOAT Statistics::get_count() const +{ + return m_statistics_base.get_count(); +} + +template +inline FLOAT Statistics::get_deviation() const +{ + // m_variance can become negative (due to rounding errors?) + return m_variance < 0 ? 0 : sqrt(m_variance); +} + +template +FLOAT Statistics::get_error() const +{ + auto count = get_count(); + return count == 0 ? 0 : get_deviation() / sqrt(count); +} + +template +inline FLOAT Statistics::get_mean() const +{ + return m_statistics_base.get_mean(); +} + +template +inline FLOAT Statistics::get_variance() const +{ + return m_variance; +} + +template +void Statistics::write(ostream& out, bool fixed, + unsigned precision) const +{ + FmtSaver saver(out); + if (fixed) + out << std::fixed; + out << setprecision(precision) << get_mean() << " dev=" + << get_deviation(); +} + +//---------------------------------------------------------------------------- + +template +class StatisticsExt +{ +public: + explicit StatisticsExt(FLOAT init_val = 0); + + void add(FLOAT val); + + void clear(FLOAT init_val = 0); + + FLOAT get_mean() const; + + FLOAT get_error() const; + + FLOAT get_count() const; + + FLOAT get_max() const; + + FLOAT get_min() const; + + FLOAT get_deviation() const; + + FLOAT get_variance() const; + + void write(ostream& out, bool fixed = false, unsigned precision = 6, + bool integer_values = false, bool with_error = false) const; + + string to_string(bool fixed = false, unsigned precision = 6, + bool integer_values = false, + bool with_error = false) const; + +private: + Statistics m_statistics; + + FLOAT m_max; + + FLOAT m_min; +}; + +template +inline StatisticsExt::StatisticsExt(FLOAT init_val) +{ + clear(init_val); +} + +template +void StatisticsExt::add(FLOAT val) +{ + m_statistics.add(val); + if (val > m_max) + m_max = val; + if (val < m_min) + m_min = val; +} + +template +inline void StatisticsExt::clear(FLOAT init_val) +{ + m_statistics.clear(init_val); + m_min = numeric_limits::max(); + m_max = -numeric_limits::max(); +} + +template +inline FLOAT StatisticsExt::get_count() const +{ + return m_statistics.get_count(); +} + +template +inline FLOAT StatisticsExt::get_deviation() const +{ + return m_statistics.get_deviation(); +} + +template +inline FLOAT StatisticsExt::get_error() const +{ + return m_statistics.get_error(); +} + +template +inline FLOAT StatisticsExt::get_max() const +{ + return m_max; +} + +template +inline FLOAT StatisticsExt::get_mean() const +{ + return m_statistics.get_mean(); +} + +template +inline FLOAT StatisticsExt::get_min() const +{ + return m_min; +} + +template +inline FLOAT StatisticsExt::get_variance() const +{ + return m_statistics.get_variance(); +} + +template +string StatisticsExt::to_string(bool fixed, unsigned precision, + bool integer_values, + bool with_error) const +{ + ostringstream s; + write(s, fixed, precision, integer_values, with_error); + return s.str(); +} + +template +void StatisticsExt::write(ostream& out, bool fixed, unsigned precision, + bool integer_values, bool with_error) const +{ + FmtSaver saver(out); + out << setprecision(precision); + if (fixed) + out << std::fixed; + out << get_mean(); + if (with_error) + out << "+-" << get_error(); + out << " dev=" << get_deviation(); + if (integer_values) + out << setprecision(0); + out << " min="; + if (m_min == numeric_limits::max()) + out << "-"; + else + out << m_min; + out << " max="; + if (m_max == -numeric_limits::max()) + out << "-"; + else + out << m_max; +} + +//---------------------------------------------------------------------------- + +/** Like StatisticsBase, but for lock-free multithreading with potentially + lost updates. + Updates and accesses of the moving average and the count are atomic but + not synchronized and use memory_order_relaxed. Therefore, updates can be + lost. Initializing via the constructor, operator= or clear() uses + memory_order_seq_cst */ +template +class StatisticsDirtyLockFree +{ +public: + /** Constructor. + @param init_val See StatisticBase::StatisticBase() */ + explicit StatisticsDirtyLockFree(FLOAT init_val = 0); + + StatisticsDirtyLockFree& operator=(const StatisticsDirtyLockFree& s); + + void add(FLOAT val, FLOAT weight = 1); + + void clear(FLOAT init_val = 0); + + void init(FLOAT mean, FLOAT count); + + FLOAT get_count() const; + + FLOAT get_mean() const; + + void write(ostream& out, bool fixed = false, + unsigned precision = 6) const; + +private: + atomic m_count; + + atomic m_mean; +}; + +template +inline StatisticsDirtyLockFree::StatisticsDirtyLockFree(FLOAT init_val) +{ + clear(init_val); +} + +template +StatisticsDirtyLockFree& +StatisticsDirtyLockFree::operator=(const StatisticsDirtyLockFree& s) +{ + m_count = s.m_count.load(); + m_mean = s.m_mean.load(); + return *this; +} + +template +void StatisticsDirtyLockFree::add(FLOAT val, FLOAT weight) +{ + FLOAT count = m_count.load(memory_order_relaxed); + FLOAT mean = m_mean.load(memory_order_relaxed); + count += weight; + mean += weight * (val - mean) / count; + m_mean.store(mean, memory_order_relaxed); + m_count.store(count, memory_order_relaxed); +} + +template +inline void StatisticsDirtyLockFree::clear(FLOAT init_val) +{ + init(init_val, 0); +} + +template +inline FLOAT StatisticsDirtyLockFree::get_count() const +{ + return m_count.load(memory_order_relaxed); +} + +template +inline FLOAT StatisticsDirtyLockFree::get_mean() const +{ + return m_mean.load(memory_order_relaxed); +} + +template +inline void StatisticsDirtyLockFree::init(FLOAT mean, FLOAT count) +{ + m_count = count; + m_mean = mean; +} + +template +void StatisticsDirtyLockFree::write(ostream& out, bool fixed, + unsigned precision) const +{ + FmtSaver saver(out); + if (fixed) + out << std::fixed; + out << setprecision(precision) << get_mean(); +} + +//---------------------------------------------------------------------------- + +template +inline ostream& operator<<(ostream& out, const StatisticsExt& s) +{ + s.write(out); + return out; +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_STATISTICS_H diff --git a/src/libboardgame_util/StringUtil.cpp b/src/libboardgame_util/StringUtil.cpp new file mode 100644 index 0000000..f95529a --- /dev/null +++ b/src/libboardgame_util/StringUtil.cpp @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/StringUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "StringUtil.h" + +#include +#include + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +template<> +bool from_string(const string& s, string& t) +{ + t = s; + return true; +} + +string get_letter_coord(unsigned i) +{ + string result; + while (true) + { + result.insert(0, 1, char('a' + i % 26)); + i /= 26; + if (i == 0) + break; + --i; + } + return result; +} + +vector split(const string& s, char separator) +{ + vector result; + string current; + for (char c : s) + { + if (c == separator) + { + result.push_back(current); + current.clear(); + continue; + } + current.push_back(c); + } + if (! current.empty() || ! result.empty()) + result.push_back(current); + return result; +} + +string time_to_string(double seconds, bool with_seconds_as_double) +{ + int int_seconds = int(seconds + 0.5); + int hours = int_seconds / 3600; + int_seconds -= hours * 3600; + int minutes = int_seconds / 60; + int_seconds -= minutes * 60; + ostringstream s; + s << setfill('0'); + if (hours > 0) + s << hours << ':'; + s << setw(2) << minutes << ':' << setw(2) << int_seconds; + if (with_seconds_as_double) + s << " (" << seconds << ')'; + return s.str(); +} + +string to_lower(string s) +{ + for (auto& c : s) + c = static_cast(tolower(c)); + return s; +} + +string trim(const string& s) +{ + string::size_type begin = 0; + auto end = s.size(); + while (begin != end && isspace(s[begin])) + ++begin; + while (end > begin && isspace(s[end - 1])) + --end; + return s.substr(begin, end - begin); +} + +string trim_right(const string& s) +{ + auto end = s.size(); + while (end > 0 && isspace(s[end - 1])) + --end; + return s.substr(0, end); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/StringUtil.h b/src/libboardgame_util/StringUtil.h new file mode 100644 index 0000000..65fb83b --- /dev/null +++ b/src/libboardgame_util/StringUtil.h @@ -0,0 +1,58 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/StringUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_STRING_UTIL_H +#define LIBBOARDGAME_UTIL_STRING_UTIL_H + +#include +#include +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +template +bool from_string(const string& s, T& t) +{ + istringstream in(s); + in >> t; + return ! in.fail(); +} + +template<> +bool from_string(const string& s, string& t); + +/** Get a letter representing a coordinate. + Returns 'a' to 'z' for i between 0 and 25 and continues with 'aa','ab'... + for coordinates larger than 25. */ +string get_letter_coord(unsigned i); + +vector split(const string& s, char separator); + +string time_to_string(double seconds, bool with_seconds_as_double = false); + +template +string to_string(const T& t) +{ + ostringstream buffer; + buffer << t; + return buffer.str(); +} + +string to_lower(string s); + +string trim(const string& s); + +string trim_right(const string& s); + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_STRING_UTIL_H diff --git a/src/libboardgame_util/TimeIntervalChecker.cpp b/src/libboardgame_util/TimeIntervalChecker.cpp new file mode 100644 index 0000000..0877e6d --- /dev/null +++ b/src/libboardgame_util/TimeIntervalChecker.cpp @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @file TimeIntervalChecker.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TimeIntervalChecker.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +TimeIntervalChecker::TimeIntervalChecker(TimeSource& time_source, + double time_interval, + double max_time) + : IntervalChecker(time_source, time_interval, + bind(&TimeIntervalChecker::check_time, this)), + m_max_time(max_time), + m_start_time(m_time_source()) +{ +} + +TimeIntervalChecker::TimeIntervalChecker(TimeSource& time_source, + double max_time) + : IntervalChecker(time_source, max_time > 1 ? 0.1 : 0.1 * max_time, + bind(&TimeIntervalChecker::check_time, this)), + m_max_time(max_time), + m_start_time(m_time_source()) +{ +} + +bool TimeIntervalChecker::check_time() +{ + return m_time_source() - m_start_time > m_max_time; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/TimeIntervalChecker.h b/src/libboardgame_util/TimeIntervalChecker.h new file mode 100644 index 0000000..d898b02 --- /dev/null +++ b/src/libboardgame_util/TimeIntervalChecker.h @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +/** @file TimeIntervalChecker.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_TIME_INTERVAL_CHECKER_H +#define LIBBOARDGAME_UTIL_TIME_INTERVAL_CHECKER_H + +#include "IntervalChecker.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +/** IntervalChecker that checks if a maximum total time was reached. */ +class TimeIntervalChecker + : public IntervalChecker +{ +public: + TimeIntervalChecker(TimeSource& time_source, double time_interval, + double max_time); + + /** Constructor with automatically set time_interval. + The time interval will be set to 0.1, if max_time > 1, otherwise + to 0.1 * max_time */ + TimeIntervalChecker(TimeSource& time_source, double max_time); + +private: + double m_max_time; + + double m_start_time; + + bool check_time(); +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_TIME_INTERVAL_CHECKER_H diff --git a/src/libboardgame_util/TimeSource.cpp b/src/libboardgame_util/TimeSource.cpp new file mode 100644 index 0000000..43f2a24 --- /dev/null +++ b/src/libboardgame_util/TimeSource.cpp @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/TimeSource.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TimeSource.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +TimeSource::~TimeSource() +{ +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/TimeSource.h b/src/libboardgame_util/TimeSource.h new file mode 100644 index 0000000..bc040fd --- /dev/null +++ b/src/libboardgame_util/TimeSource.h @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/TimeSource.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_TIME_SOURCE_H +#define LIBBOARDGAME_UTIL_TIME_SOURCE_H + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +/** Abstract time source for measuring thinking times for move generation. + Typical implementations are wall time, CPU time or mock time sources + for unit tests. They do not need to provide high resolutions (but should + support at least 100 ms) and should support maximum times of days (or even + months). + @ref libboardgame_doc_threadsafe_after_construction */ +class TimeSource +{ +public: + virtual ~TimeSource(); + + /** Get the current time in seconds. */ + virtual double operator()() = 0; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_TIME_SOURCE_H diff --git a/src/libboardgame_util/Timer.cpp b/src/libboardgame_util/Timer.cpp new file mode 100644 index 0000000..68a8a3e --- /dev/null +++ b/src/libboardgame_util/Timer.cpp @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Timer.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Timer.h" + +#include "Assert.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +Timer::Timer(TimeSource& time_source) + : m_start(time_source()), + m_time_source(&time_source) +{ } + +double Timer::operator()() const +{ + LIBBOARDGAME_ASSERT(m_time_source); + return (*m_time_source)() - m_start; +} + +void Timer::reset() +{ + m_start = (*m_time_source)(); +} + +void Timer::reset(TimeSource& time_source) +{ + m_time_source = &time_source; + reset(); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Timer.h b/src/libboardgame_util/Timer.h new file mode 100644 index 0000000..13a23b9 --- /dev/null +++ b/src/libboardgame_util/Timer.h @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Timer.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_TIMER_H +#define LIBBOARDGAME_UTIL_TIMER_H + +#include "TimeSource.h" + +namespace libboardgame_util { + +class Timer +{ +public: + /** Constructor without time source. + If constructed without time source, the timer cannot be used before + reset(TimeSource&) was called. */ + Timer() = default; + + /** Constructor. + @param time_source (@ref libboardgame_doc_storesref) */ + explicit Timer(TimeSource& time_source); + + double operator()() const; + + void reset(); + + void reset(TimeSource& time_source); + +private: + double m_start; + + TimeSource* m_time_source = nullptr; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_TIMER_H diff --git a/src/libboardgame_util/Unused.h b/src/libboardgame_util/Unused.h new file mode 100644 index 0000000..43be3cf --- /dev/null +++ b/src/libboardgame_util/Unused.h @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Unused.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_UNUSED_H +#define LIBBOARDGAME_UTIL_UNUSED_H + +//----------------------------------------------------------------------------- + +template static void LIBBOARDGAME_UNUSED(const T&) { } + +#if LIBBOARDGAME_DEBUG +#define LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(x) +#else +#define LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(x) LIBBOARDGAME_UNUSED(x) +#endif + +//----------------------------------------------------------------------------- + +#endif // LIBBOARDGAME_UTIL_UNUSED_H diff --git a/src/libboardgame_util/WallTimeSource.cpp b/src/libboardgame_util/WallTimeSource.cpp new file mode 100644 index 0000000..754e20f --- /dev/null +++ b/src/libboardgame_util/WallTimeSource.cpp @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/WallTimeSource.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "WallTimeSource.h" + +#include + +namespace libboardgame_util { + +using namespace std::chrono; + +//----------------------------------------------------------------------------- + +double WallTimeSource::operator()() +{ + auto t = system_clock::now().time_since_epoch(); + return duration_cast>(t).count(); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/WallTimeSource.h b/src/libboardgame_util/WallTimeSource.h new file mode 100644 index 0000000..9c99371 --- /dev/null +++ b/src/libboardgame_util/WallTimeSource.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/WallTimeSource.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_WALL_TIME_SOURCE_H +#define LIBBOARDGAME_UTIL_WALL_TIME_SOURCE_H + +#include "TimeSource.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +/** Wall time. + @ref libboardgame_doc_threadsafe_after_construction */ +class WallTimeSource + : public TimeSource +{ +public: + double operator()() override; +}; +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_WALL_TIME_SOURCE_H diff --git a/src/libpentobi_base/Board.cpp b/src/libpentobi_base/Board.cpp new file mode 100644 index 0000000..a06ddcf --- /dev/null +++ b/src/libpentobi_base/Board.cpp @@ -0,0 +1,799 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Board.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Board.h" + +#include +#include "CallistoGeometry.h" +#include "MoveMarker.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +namespace { + +void write_x_coord(ostream& out, unsigned width, unsigned offset) +{ + for (unsigned i = 0; i < offset; ++i) + out << ' '; + char c = 'A'; + for (unsigned x = 0; x < width; ++x, ++c) + { + if (x < 26) + out << ' '; + else + out << 'A'; + if (x == 26) + c = 'A'; + out << c; + } + out << '\n'; +} + +void set_color(ostream& out, const char* esc_sequence) +{ + if (Board::color_output) + out << esc_sequence; +} + +} // namespace + +//----------------------------------------------------------------------------- + +bool Board::color_output = false; + +Board::Board(Variant variant) +{ + m_color_char[Color(0)] = 'X'; + m_color_char[Color(1)] = 'O'; + m_color_char[Color(2)] = '#'; + m_color_char[Color(3)] = '@'; + for_each_color([&](Color c) { + m_state_color[c].forbidden[Point::null()] = false; + }); + init_variant(variant); + init(); +#if LIBBOARDGAME_DEBUG + m_snapshot.moves_size = + numeric_limits::max(); +#endif +} + +void Board::copy_from(const Board& bd) +{ + if (m_variant != bd.m_variant) + init_variant(bd.m_variant); + m_moves = bd.m_moves; + m_setup.to_play = bd.m_setup.to_play; + m_state_base = bd.m_state_base; + for (Color c : get_colors()) + { + m_state_color[c] = bd.m_state_color[c]; + m_setup.placements[c] = bd.m_setup.placements[c]; + m_attach_points[c] = bd.m_attach_points[c]; + } +} + +const Transform* Board::find_transform(Move mv) const +{ + auto& geo = get_geometry(); + PiecePoints points; + for (Point p : get_move_points(mv)) + points.push_back(CoordPoint(geo.get_x(p), geo.get_y(p))); + return get_piece_info(get_move_piece(mv)).find_transform(geo, points); +} + +void Board::gen_moves(Color c, MoveMarker& marker, MoveList& moves) const +{ + moves.clear(); + bool is_callisto = (m_piece_set == PieceSet::callisto); + if (! is_callisto && is_first_piece(c)) + { + for (Point p : get_starting_points(c)) + if (! m_state_color[c].forbidden[p]) + { + auto adj_status = get_adj_status(p, c); + for (Piece piece : m_state_color[c].pieces_left) + gen_moves(c, p, piece, adj_status, marker, moves); + } + return; + } + if (is_callisto && is_piece_left(c, m_one_piece)) + for (auto p : *m_geo) + if (! is_forbidden(p, c) && ! m_is_center_section[p]) + gen_moves(c, p, m_one_piece, get_adj_status(p, c), marker, + moves); + for (Point p : get_attach_points(c)) + if (! m_state_color[c].forbidden[p]) + { + auto adj_status = get_adj_status(p, c); + for (Piece piece : m_state_color[c].pieces_left) + if (! is_callisto || piece != m_one_piece) + gen_moves(c, p, piece, adj_status, marker, moves); + } +} + +void Board::gen_moves(Color c, Point p, Piece piece, unsigned adj_status, + MoveMarker& marker, MoveList& moves) const +{ + for (Move mv : m_bc->get_moves(piece, p, adj_status)) + if (! marker[mv] && ! is_forbidden(c, mv)) + { + moves.push_back(mv); + marker.set(mv); + } +} + +ScoreType Board::get_bonus(Color c) const +{ + if (get_pieces_left(c).size() > 0) + return 0; + auto bonus = m_bonus_all_pieces; + unsigned i = m_moves.size(); + while (i > 0) + { + --i; + if (m_moves[i].color == c) + { + auto piece = get_move_piece(m_moves[i].move); + if (m_score_points[piece] == 1) + bonus += m_bonus_one_piece; + break; + } + } + return bonus; +} + +Color Board::get_effective_to_play() const +{ + return get_effective_to_play(get_to_play()); +} + +Color Board::get_effective_to_play(Color c) const +{ + Color result = c; + do + { + if (has_moves(result)) + return result; + result = get_next(result); + } + while (result != c); + return result; +} + +void Board::get_place(Color c, unsigned& place, bool& is_shared) const +{ + bool break_ties = (m_piece_set == PieceSet::callisto); + array all_scores; + for (Color::IntType i = 0; i < Color::range; ++i) + { + all_scores[i] = get_score(Color(i)); + if (break_ties) + all_scores[i] += i * 0.0001f; + } + auto score = all_scores[c.to_int()]; + sort(all_scores.begin(), all_scores.begin() + m_nu_players, + greater()); + is_shared = false; + bool found = false; + for (unsigned i = 0; i < m_nu_players; ++i) + if (all_scores[i] == score) + { + if (! found) + { + place = i; + found = true; + } + else + is_shared = true; + } +} + +Move Board::get_move_at(Point p) const +{ + auto s = get_point_state(p); + if (s.is_color()) + { + auto c = s.to_color(); + for (Move mv : m_setup.placements[c]) + if (get_move_points(mv).contains(p)) + return mv; + for (ColorMove color_mv : m_moves) + if (color_mv.color == c) + { + Move mv = color_mv.move; + if (get_move_points(mv).contains(p)) + return mv; + } + } + return Move::null(); +} + +bool Board::has_moves(Color c) const +{ + bool is_callisto = (m_piece_set == PieceSet::callisto); + if (is_callisto && is_piece_left(c, m_one_piece)) + for (auto p : *m_geo) + if (! is_forbidden(p, c) && ! m_is_center_section[p]) + return true; + if (! is_callisto && is_first_piece(c)) + { + for (auto p : get_starting_points(c)) + if (has_moves(c, p)) + return true; + return false; + } + for (auto p : get_attach_points(c)) + if (has_moves(c, p)) + return true; + return false; +} + +bool Board::has_moves(Color c, Point p) const +{ + if (is_forbidden(p, c)) + return false; + bool is_callisto = (m_piece_set == PieceSet::callisto); + if (is_callisto && is_piece_left(c, m_one_piece)) + if (m_is_center_section[p]) + return true; + auto adj_status = get_adj_status(p, c); + for (auto piece : m_state_color[c].pieces_left) + { + if (piece == m_one_piece && is_callisto) + continue; + for (auto mv : m_bc->get_moves(piece, p, adj_status)) + if (! is_forbidden(c, mv)) + return true; + } + return false; +} + +bool Board::has_setup() const +{ + for (Color c : get_colors()) + if (! m_setup.placements[c].empty()) + return true; + return false; +} + +void Board::init(Variant variant, const Setup* setup) +{ + if (variant != m_variant) + init_variant(variant); + + // If you make changes here, make sure that you also update copy_from() + + m_state_base.point_state.fill(PointState::empty(), *m_geo); + for (Color c : get_colors()) + { + auto& state = m_state_color[c]; + state.forbidden.fill(false, *m_geo); + state.is_attach_point.fill(false, *m_geo); + state.pieces_left.clear(); + state.nu_onboard_pieces = 0; + state.points = 0; + for (Piece::IntType i = 0; i < get_nu_uniq_pieces(); ++i) + { + Piece piece(i); + state.pieces_left.push_back(piece); + state.nu_left_piece[piece] = + static_cast(get_nu_piece_instances(piece)); + } + m_attach_points[c].clear(); + } + m_state_base.nu_onboard_pieces_all = 0; + if (! setup) + { + m_setup.clear(); + m_state_base.to_play = Color(0); + } + else + { + m_setup = *setup; + place_setup(m_setup); + m_state_base.to_play = setup->to_play; + optimize_attach_point_lists(); + for (Color c : get_colors()) + if (m_state_color[c].pieces_left.empty()) + m_state_color[c].points += m_bonus_all_pieces; + } + m_moves.clear(); +} + +void Board::init_variant(Variant variant) +{ + m_variant = variant; + m_nu_colors = libpentobi_base::get_nu_colors(variant); + if (m_nu_colors == 2) + { + m_color_name[Color(0)] = "Blue"; + m_color_name[Color(1)] = "Green"; + m_color_esc_sequence[Color(0)] = "\x1B[1;34;47m"; + m_color_esc_sequence[Color(1)] = "\x1B[1;32;47m"; + m_color_esc_sequence_text[Color(0)] = "\x1B[1;34m"; + m_color_esc_sequence_text[Color(1)] = "\x1B[1;32m"; + } + else + { + m_color_name[Color(0)] = "Blue"; + m_color_name[Color(1)] = "Yellow"; + m_color_name[Color(2)] = "Red"; + m_color_name[Color(3)] = "Green"; + m_color_esc_sequence[Color(0)] = "\x1B[1;34;47m"; + m_color_esc_sequence[Color(1)] = "\x1B[1;33;47m"; + m_color_esc_sequence[Color(2)] = "\x1B[1;31;47m"; + m_color_esc_sequence[Color(3)] = "\x1B[1;32;47m"; + m_color_esc_sequence_text[Color(0)] = "\x1B[1;34m"; + m_color_esc_sequence_text[Color(1)] = "\x1B[1;33m"; + m_color_esc_sequence_text[Color(2)] = "\x1B[1;31m"; + m_color_esc_sequence_text[Color(3)] = "\x1B[1;32m"; + } + m_nu_players = libpentobi_base::get_nu_players(variant); + m_bc = &BoardConst::get(variant); + m_piece_set = m_bc->get_piece_set(); + m_is_callisto = (m_piece_set == PieceSet::callisto); + if ((m_piece_set == PieceSet::classic && variant != Variant::junior) + || m_piece_set == PieceSet::trigon) + { + m_bonus_all_pieces = 15; + m_bonus_one_piece = 5; + } + else if (m_piece_set == PieceSet::nexos) + { + m_bonus_all_pieces = 10; + m_bonus_one_piece = 0; + } + else + { + m_bonus_all_pieces = 0; + m_bonus_one_piece = 0; + } + m_max_piece_size = m_bc->get_max_piece_size(); + m_max_adj_attach = m_bc->get_max_adj_attach(); + m_geo = &m_bc->get_geometry(); + m_move_info_array = m_bc->get_move_info_array(); + m_move_info_ext_array = m_bc->get_move_info_ext_array(); + m_move_info_ext_2_array = m_bc->get_move_info_ext_2_array(); + m_starting_points.init(variant, *m_geo); + if (m_piece_set == PieceSet::callisto) + for (Point p : *m_geo) + m_is_center_section[p] = + CallistoGeometry::is_center_section(m_geo->get_x(p), + m_geo->get_y(p), + m_nu_players); + else + m_is_center_section.fill(false, *m_geo); + for (Color c : get_colors()) + { + if (m_nu_players == 2 && m_nu_colors == 4) + m_second_color[c] = get_next(get_next(c)); + else + m_second_color[c] = c; + } + for (Piece::IntType i = 0; i < get_nu_uniq_pieces(); ++i) + { + Piece piece(i); + auto& piece_info = get_piece_info(piece); + m_score_points[piece] = piece_info.get_score_points(); + if (piece_info.get_points().size() == 1) + m_one_piece = piece; + } +} + +bool Board::is_game_over() const +{ + for (Color c : get_colors()) + if (has_moves(c)) + return false; + return true; +} + +bool Board::is_legal(Color c, Move mv) const +{ + auto piece = get_move_piece(mv); + if (! is_piece_left(c, piece)) + return false; + auto points = get_move_points(mv); + auto i = points.begin(); + auto end = points.end(); + bool has_attach_point = false; + do + { + if (m_state_color[c].forbidden[*i]) + return false; + if (is_attach_point(*i, c)) + has_attach_point = true; + } + while (++i != end); + if (m_is_callisto) + { + if (m_state_color[c].nu_left_piece[m_one_piece] > 1 + && piece != m_one_piece) + return false; + if (piece == m_one_piece) + return ! m_is_center_section[*points.begin()]; + } + if (has_attach_point) + return true; + if (! is_first_piece(c)) + return false; + i = points.begin(); + do + if (is_colorless_starting_point(*i) + || (is_colored_starting_point(*i) + && get_starting_point_color(*i) == c)) + return true; + while (++i != end); + return false; +} + +/** Remove forbidden points from attach point lists. + The attach point lists do not guarantee that they contain only + non-forbidden attach points because that would be too expensive to + update incrementally but at certain times that are not performance + critical (e.g. before taking a snapshot), we can remove them. */ +void Board::optimize_attach_point_lists() +{ + PointList l; + for (Color c : get_colors()) + { + l.clear(); + for (Point p : m_attach_points[c]) + if (! is_forbidden(p, c)) + l.push_back(p); + m_attach_points[c] = l; + } +} + +/** Place setup moves on board. */ +void Board::place_setup(const Setup& setup) +{ + if (m_max_piece_size == 5) + for (Color c : get_colors()) + for (Move mv : setup.placements[c]) + place<5, 16>(c, mv); + else if (m_max_piece_size == 6) + for (Color c : get_colors()) + for (Move mv : setup.placements[c]) + place<6, 22>(c, mv); + else + for (Color c : get_colors()) + for (Move mv : setup.placements[c]) + place<7, 12>(c, mv); +} + +void Board::play(Color c, Move mv) +{ + if (m_max_piece_size == 5) + play<5, 16>(c, mv); + else if (m_max_piece_size == 6) + play<6, 22>(c, mv); + else + play<7, 12>(c, mv); +} + +void Board::take_snapshot() +{ + optimize_attach_point_lists(); + m_snapshot.moves_size = m_moves.size(); + m_snapshot.state_base.to_play = m_state_base.to_play; + m_snapshot.state_base.nu_onboard_pieces_all = + m_state_base.nu_onboard_pieces_all; + m_snapshot.state_base.point_state.copy_from(m_state_base.point_state, + *m_geo); + for (Color c : get_colors()) + { + m_snapshot.attach_points_size[c] = m_attach_points[c].size(); + const auto& state = m_state_color[c]; + auto& snapshot_state = m_snapshot.state_color[c]; + snapshot_state.forbidden.copy_from(state.forbidden, *m_geo); + snapshot_state.is_attach_point.copy_from(state.is_attach_point, + *m_geo); + snapshot_state.pieces_left = state.pieces_left; + snapshot_state.nu_left_piece = state.nu_left_piece; + snapshot_state.nu_onboard_pieces = state.nu_onboard_pieces; + snapshot_state.points = state.points; + } +} + +void Board::write(ostream& out, bool mark_last_move) const +{ + // Sort lists of left pieces by name + ColorMap pieces_left; + for (Color c : get_colors()) + { + pieces_left[c] = m_state_color[c].pieces_left; + sort(pieces_left[c].begin(), pieces_left[c].end(), + [&](Piece p1, Piece p2) + { + return + get_piece_info(p1).get_name() + < get_piece_info(p2).get_name(); + }); + } + + ColorMove last_mv = ColorMove::null(); + if (mark_last_move) + { + unsigned n = get_nu_moves(); + if (n > 0) + last_mv = get_move(n - 1); + } + unsigned width = m_geo->get_width(); + unsigned height = m_geo->get_height(); + bool is_info_location_right = (width <= 20); + bool is_trigon = (m_piece_set == PieceSet::trigon); + bool is_nexos = (m_piece_set == PieceSet::nexos); + bool is_callisto = (m_piece_set == PieceSet::callisto); + for (unsigned y = 0; y < height; ++y) + { + if (height - y < 10) + out << ' '; + out << (height - y) << ' '; + for (unsigned x = 0; x < width; ++x) + { + Point p = m_geo->get_point(x, y); + bool is_offboard = p.is_null(); + auto point_type = m_geo->get_point_type(x, y); + if ((x > 0 || (is_trigon && x == 0 && m_geo->is_onboard(x + 1, y))) + && ! is_offboard) + { + // Print a space horizontally between fields on the board. On a + // Trigon board, a slash or backslash is used instead of the + // space to indicate the orientation of the triangles. A + // less-than/greater-than character is used instead of the + // space to mark the last piece played. + if (! last_mv.is_null() + && get_move_points(last_mv.move).contains(p) + && (x == 0 || ! m_geo->is_onboard(x - 1, y) + || get_point_state(m_geo->get_point(x - 1, y)) + != last_mv.color)) + { + set_color(out, "\x1B[1;37;47m"); + out << '>'; + last_mv = ColorMove::null(); + } + else if (! last_mv.is_null() + && x > 0 && m_geo->is_onboard(x - 1, y) + && get_move_points(last_mv.move).contains( + m_geo->get_point(x - 1, y)) + && get_point_state(p) != last_mv.color + && get_point_state(m_geo->get_point(x - 1, y)) + == last_mv.color) + { + set_color(out, "\x1B[1;37;47m"); + out << '<'; + last_mv = ColorMove::null(); + } + else if (is_trigon) + { + set_color(out, "\x1B[1;30;47m"); + out << (point_type == 1 ? '\\' : '/'); + } + else + { + set_color(out, "\x1B[1;30;47m"); + out << ' '; + } + } + if (is_offboard) + { + if (is_trigon && x > 0 && m_geo->is_onboard(x - 1, y)) + { + set_color(out, "\x1B[1;30;47m"); + out << (point_type == 1 ? '\\' : '/'); + } + else if (is_callisto && x == 0) + { + set_color(out, "\x1B[0m"); + out << ' '; + } + else + { + set_color(out, is_nexos ? "\x1B[1;30;47m" : "\x1B[0m"); + out << " "; + } + } + else + { + PointState s = get_point_state(p); + if (s.is_empty()) + { + if (is_colored_starting_point(p) && ! is_nexos) + { + Color c = get_starting_point_color(p); + set_color(out, m_color_esc_sequence[c]); + out << '+'; + } + else if (is_colorless_starting_point(p)) + { + set_color(out, "\x1B[1;30;47m"); + out << '+'; + } + else + { + set_color(out, "\x1B[1;30;47m"); + if (is_trigon) + out << ' '; + else if (is_nexos && point_type == 1) + out << '-'; + else if (is_nexos && point_type == 2) + out << '|'; + else if (is_nexos && point_type == 0) + out << '+'; + else if (is_callisto && is_center_section(p)) + out << ','; + else + out << '.'; + } + } + else + { + Color color = s.to_color(); + set_color(out, m_color_esc_sequence[color]); + if (is_nexos && m_geo->get_point_type(p) == 0) + out << '*'; // Uncrossable junction + else + out << m_color_char[color]; + } + } + } + if (is_trigon) + { + if (m_geo->is_onboard(width - 1, y)) + { + set_color(out, "\x1B[1;30;47m"); + out << (m_geo->get_point_type(width - 1, y) != 1 ? '\\' : '/'); + } + else + { + set_color(out, "\x1B[0m"); + out << " "; + } + } + set_color(out, "\x1B[0m"); + if (is_info_location_right) + write_info_line(out, y, pieces_left); + out << '\n'; + } + write_x_coord(out, width, is_trigon ? 3 : 2); + if (! is_info_location_right) + for (Color c : get_colors()) + { + write_color_info_line1(out, c); + out << " "; + write_color_info_line2(out, c, pieces_left[c]); + out << ' '; + write_color_info_line3(out, c, pieces_left[c]); + out << '\n'; + } +} + +void Board::write_color_info_line1(ostream& out, Color c) const +{ + set_color(out, m_color_esc_sequence_text[c]); + if (! is_game_over() && get_effective_to_play() == c) + out << '(' << (get_nu_moves() + 1) << ") "; + out << m_color_name[c] << "(" << m_color_char[c] << "): " << get_points(c); + if (! has_moves(c)) + out << '!'; + set_color(out, "\x1B[0m"); +} + +void Board::write_color_info_line2(ostream& out, Color c, + const PiecesLeftList& pieces_left) const +{ + if (m_variant == Variant::junior) + write_pieces_left(out, c, pieces_left, 0, 6); + else + write_pieces_left(out, c, pieces_left, 0, 10); +} + +void Board::write_color_info_line3(ostream& out, Color c, + const PiecesLeftList& pieces_left) const +{ + if (m_variant == Variant::junior) + write_pieces_left(out, c, pieces_left, 6, get_nu_uniq_pieces()); + else + write_pieces_left(out, c, pieces_left, 10, get_nu_uniq_pieces()); +} + +void Board::write_info_line(ostream& out, unsigned y, + const ColorMap& pieces_left) const +{ + if (y == 0) + { + out << " "; + write_color_info_line1(out, Color(0)); + } + else if (y == 1) + { + out << " "; + write_color_info_line2(out, Color(0), pieces_left[Color(0)]); + } + else if (y == 2) + { + out << " "; + write_color_info_line3(out, Color(0), pieces_left[Color(0)]); + } + else if (y == 4) + { + out << " "; + write_color_info_line1(out, Color(1)); + } + else if (y == 5) + { + out << " "; + write_color_info_line2(out, Color(1), pieces_left[Color(1)]); + } + else if (y == 6) + { + out << " "; + write_color_info_line3(out, Color(1), pieces_left[Color(1)]); + } + else if (y == 8 && m_nu_colors > 2) + { + out << " "; + write_color_info_line1(out, Color(2)); + } + else if (y == 9 && m_nu_colors > 2) + { + out << " "; + write_color_info_line2(out, Color(2), pieces_left[Color(2)]); + } + else if (y == 10 && m_nu_colors > 2) + { + out << " "; + write_color_info_line3(out, Color(2), pieces_left[Color(2)]); + } + else if (y == 12 && m_nu_colors > 3) + { + out << " "; + write_color_info_line1(out, Color(3)); + } + else if (y == 13 && m_nu_colors > 3) + { + out << " "; + write_color_info_line2(out, Color(3), pieces_left[Color(3)]); + } + else if (y == 14 && m_nu_colors > 3) + { + out << " "; + write_color_info_line3(out, Color(3), pieces_left[Color(3)]); + } +} + +void Board::write_pieces_left(ostream& out, Color c, + const PiecesLeftList& pieces_left, + unsigned begin, unsigned end) const +{ + for (unsigned i = begin; i < end; ++i) + if (i < pieces_left.size()) + { + if (i > begin) + out << ' '; + Piece piece = pieces_left[i]; + auto& name = get_piece_info(piece).get_name(); + unsigned nu_left = m_state_color[c].nu_left_piece[piece]; + for (unsigned j = 0; j < nu_left; ++j) + { + if (j > 0) + out << ' '; + out << name; + } + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Board.h b/src/libpentobi_base/Board.h new file mode 100644 index 0000000..9217832 --- /dev/null +++ b/src/libpentobi_base/Board.h @@ -0,0 +1,901 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Board.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_BOARD_H +#define LIBPENTOBI_BASE_BOARD_H + +#include "BoardConst.h" +#include "ColorMap.h" +#include "ColorMove.h" +#include "Variant.h" +#include "Geometry.h" +#include "Grid.h" +#include "MoveList.h" +#include "PointList.h" +#include "PointState.h" +#include "Setup.h" +#include "StartingPoints.h" + +namespace libpentobi_base { + +class MoveMarker; + +//----------------------------------------------------------------------------- + +/** Blokus board. + @note @ref libboardgame_avoid_stack_allocation */ +class Board +{ +public: + typedef Grid PointStateGrid; + + /** Maximum number of pieces per player in any game variant. */ + static const unsigned max_pieces = Setup::max_pieces; + + typedef ArrayList PiecesLeftList; + + static const unsigned max_player_moves = max_pieces; + + /** Maximum number of moves in any game variant. */ + static const unsigned max_game_moves = Color::range * max_player_moves; + + /** Use ANSI escape sequences for colored text output in operator>> */ + static bool color_output; + + explicit Board(Variant variant); + + /** Not implemented to avoid unintended copies. + Use copy_from() to copy a board state. */ + Board(const Board&) = delete; + + /** Not implemented to avoid unintended copies. + Use copy_from() to copy a board state. */ + Board& operator=(const Board&) = delete; + + Geometry::Iterator begin() const { return m_geo->begin(); } + + Geometry::Iterator end() const { return m_geo->end(); } + + Variant get_variant() const; + + Color::IntType get_nu_colors() const; + + Color::Range get_colors() const { return Color::Range(m_nu_colors); } + + /** Number of colors that are not played alternately. + This is equal to get_nu_colors() apart from Variant::classic_3. */ + Color::IntType get_nu_nonalt_colors() const; + + unsigned get_nu_players() const; + + Piece::IntType get_nu_uniq_pieces() const; + + /** Number of instances of a unique piece per color. */ + unsigned get_nu_piece_instances(Piece piece) const; + + Color get_next(Color c) const; + + Color get_previous(Color c) const; + + const PieceTransforms& get_transforms() const; + + /** Get the state of an on-board point. */ + PointState get_point_state(Point p) const; + + const PointStateGrid& get_point_state() const; + + /** Get next color to play. + The next color to play is the next color of the color of the last move + played even if it has no more moves to play. */ + Color get_to_play() const; + + /** Get the player who plays the next move for the 4th color in + Variant::classic_3. */ + Color::IntType get_alt_player() const; + + /** Equivalent to get_effective_to_play(get_to_play()) */ + Color get_effective_to_play() const; + + /** Get next color to play that still has moves. + Colors are tried in their playing order starting with c. If no color + has moves left, c is returned. */ + Color get_effective_to_play(Color c) const; + + const PiecesLeftList& get_pieces_left(Color c) const; + + bool is_piece_left(Color c, Piece piece) const; + + /** Check if no piece of a color has been placed on the board yet. + This includes setup pieces and played moves. */ + bool is_first_piece(Color c) const; + + /** Get number of instances left of a piece. + This value can be greater 1 in game variants that use multiple instances + of a unique piece per player. */ + unsigned get_nu_left_piece(Color c, Piece piece) const; + + /** Get number of points of a color including the bonus. */ + ScoreType get_points(Color c) const { return m_state_color[c].points; } + + /** Get number of bonus points of a color. */ + ScoreType get_bonus(Color c) const; + + /** Is a point a potential attachment point for a color. + Does not check if the point is forbidden. */ + bool is_attach_point(Point p, Color c) const; + + /** Get potential attachment points for a color. + Does not check if the point is forbidden. */ + const PointList& get_attach_points(Color c) const; + + /** Initialize the current board for a given game variant. + @param variant The game variant + @param setup An optional setup position to initialize the board + with. */ + void init(Variant variant, const Setup* setup = nullptr); + + /** Clear the current board without changing the current game variant. + See init(Variant,const Setup*) */ + void init(const Setup* setup = nullptr); + + /** Copy the board state and move history from another board. + This is like an assignment operator but because boards are rarely + copied by value and copying is expensive, it is an explicit function to + avoid accidental copying. */ + void copy_from(const Board& bd); + + /** Play a move. + @pre ! mv.is_null() + @pre get_nu_moves() < max_game_moves */ + void play(Color c, Move mv); + + /** More efficient version of play() if maximum piece size of current + game variant is known at compile time. */ + template + void play(Color c, Move mv); + + /** Play a move. + @pre ! mv.move.is_null() + @pre get_nu_moves() < max_game_moves */ + void play(ColorMove mv); + + void set_to_play(Color c); + + void write(ostream& out, bool mark_last_move = true) const; + + /** Get the setup of the board before any moves were played. + If the board was initialized without setup, the return value contains + a setup with empty placement lists and Color(0) as the color to + play. */ + const Setup& get_setup() const; + + bool has_setup() const; + + /** Get the total number of moves played by all colors. + Does not include setup pieces. + @see get_nu_onboard_pieces() */ + unsigned get_nu_moves() const; + + /** Get the number of pieces on board. + This is the number of setup pieces, if the board was initialized + with a setup position, plus the number of pieces played as moves. */ + unsigned get_nu_onboard_pieces() const; + + /** Get the number of pieces on board of a color. + This is the number of setup pieces, if the board was initialized + with a setup position, plus the number of pieces played as moves. */ + unsigned get_nu_onboard_pieces(Color c) const; + + ColorMove get_move(unsigned n) const; + + const ArrayList& get_moves() const; + + /** Generate all legal moves for a color. + @param c The color + @param marker A move marker reused for efficiency (needs to be clear) + @param[out] moves The list of moves. */ + void gen_moves(Color c, MoveMarker& marker, MoveList& moves) const; + + bool has_moves(Color c) const; + + /** Check that no color has any moves left. */ + bool is_game_over() const; + + /** Check if a move is legal. + @pre ! mv.is_null() */ + bool is_legal(Color c, Move mv) const; + + /** Check if a move is legal for the current color to play. + @pre ! mv.is_null() */ + bool is_legal(Move mv) const; + + /** Check that point is not already occupied or adjacent to own color. + Point::null() is an allowed argument and returns false. */ + bool is_forbidden(Point p, Color c) const; + + const GridExt& is_forbidden(Color c) const; + + /** Check that no points of move are already occupied or adjacent to own + color. + Does not check if the move is diagonally adjacent to an existing + occupied point of the same color. */ + bool is_forbidden(Color c, Move mv) const; + + const BoardConst& get_board_const() const { return *m_bc; } + + BoardType get_board_type() const; + + PieceSet get_piece_set() const { return m_piece_set; } + + unsigned get_adj_status(Point p, Color c) const; + + /** Is a point in the center section that is forbidden for the 1-piece in + Callisto? + Always returns false for other game variants. */ + bool is_center_section(Point p) const { return m_is_center_section[p]; } + + PrecompMoves::Range get_moves(Piece piece, Point p, + unsigned adj_status) const; + + /** Get score. + The score is the number of points for a color minus the number of + points of the opponent (or the average score of the opponents if there + are more than two players). */ + ScoreType get_score(Color c) const; + + /** Specialized version of get_score(). + @pre get_nu_colors() == 2 */ + ScoreType get_score_twocolor(Color c) const; + + /** Specialized version of get_score(). + @pre get_nu_players() == 4 && get_nu_colors() == 4 */ + ScoreType get_score_multicolor(Color c) const; + + /** Specialized version of get_score(). + @pre get_nu_players() > 2 */ + ScoreType get_score_multiplayer(Color c) const; + + /** Specialized version of get_score(). + @pre get_nu_players() == 2 */ + ScoreType get_score_twoplayer(Color c) const; + + /** Get the place of a player in the game result. + @param c The color of the player. + @param[out] place The place of the player with that color. The place + numbers start with 0. A place can be shared if several players have the + same score. If a place is shared by n players, the following n-1 places + are not used. + @param[out] is_shared True if the place was shared. */ + void get_place(Color c, unsigned& place, bool& is_shared) const; + + const Geometry& get_geometry() const { return *m_geo; } + + /** See BoardConst::to_string() */ + string to_string(Move mv, bool with_piece_name = false) const; + + /** See BoardConst::from_string() */ + Move from_string(const string& s) const; + + bool find_move(const MovePoints& points, Move& mv) const; + + bool find_move(const MovePoints& points, Piece piece, Move& mv) const; + + const Transform* find_transform(Move mv) const; + + const PieceInfo& get_piece_info(Piece piece) const; + + bool get_piece_by_name(const string& name, Piece& piece) const; + + /** The 1x1 piece. */ + Piece get_one_piece() const { return m_one_piece; } + + Range get_move_points(Move mv) const; + + Piece get_move_piece(Move mv) const; + + const MoveInfoExt2& get_move_info_ext_2(Move mv) const; + + bool is_colored_starting_point(Point p) const; + + bool is_colorless_starting_point(Point p) const; + + Color get_starting_point_color(Point p) const; + + const ArrayList& + get_starting_points(Color c) const; + + /** Get the second color in game variants in which a player plays two + colors. + @return The second color of the player that plays color c, or c if + the player plays only one color in the current game variant or + if the game variant is classic_3. */ + Color get_second_color(Color c) const; + + bool is_same_player(Color c1, Color c2) const; + + Move get_move_at(Point p) const; + + /** Remember the board state to quickly restore it later. + A snapshot can only be restored from a position that was reached + after playing moves from the snapshot position. */ + void take_snapshot(); + + /** See take_snapshot() */ + void restore_snapshot(); + +private: + /** Color-independent part of the board state. */ + struct StateBase + { + Color to_play; + + unsigned nu_onboard_pieces_all; + + PointStateGrid point_state; + }; + + /** Color-dependent part of the board state. */ + struct StateColor + { + GridExt forbidden; + + Grid is_attach_point; + + PiecesLeftList pieces_left; + + PieceMap nu_left_piece; + + unsigned nu_onboard_pieces; + + ScoreType points; + }; + + /** Snapshot for fast restoration of a previous position. */ + struct Snapshot + { + StateBase state_base; + + ColorMap state_color; + + unsigned moves_size; + + ColorMap attach_points_size; + }; + + + StateBase m_state_base; + + ColorMap m_state_color; + + Variant m_variant; + + PieceSet m_piece_set; + + Color::IntType m_nu_colors; + + bool m_is_callisto; + + unsigned m_nu_players; + + /** Caches m_bc->get_max_piece_size(). */ + unsigned m_max_piece_size; + + /** Caches m_bc->get_max_adj_attach(). */ + unsigned m_max_adj_attach; + + /** Bonus for playing all pieces. */ + ScoreType m_bonus_all_pieces; + + /** Bonus for playing the 1-piece last. */ + ScoreType m_bonus_one_piece; + + /** Caches get_piece_info(piece).get_score_points() */ + PieceMap m_score_points; + + const BoardConst* m_bc; + + /** Caches m_bc->get_move_info_array() */ + BoardConst::MoveInfoArray m_move_info_array; + + /** Caches m_bc->get_move_info_ext_array() */ + BoardConst::MoveInfoExtArray m_move_info_ext_array; + + /** Caches m_bc->get_move_info_ext_2_array() */ + const MoveInfoExt2* m_move_info_ext_2_array; + + const Geometry* m_geo; + + /** See is_center_section(). */ + Grid m_is_center_section; + + /** The 1x1 piece. */ + Piece m_one_piece; + + ColorMap m_attach_points; + + /** See get_second_color() */ + ColorMap m_second_color; + + ColorMap m_color_char; + + ColorMap m_color_esc_sequence; + + ColorMap m_color_esc_sequence_text; + + ColorMap m_color_name; + + ArrayList m_moves; + + Snapshot m_snapshot; + + Setup m_setup; + + StartingPoints m_starting_points; + + + void gen_moves(Color c, Point p, Piece piece, unsigned adj_status, + MoveMarker& marker, MoveList& moves) const; + + bool has_moves(Color c, Point p) const; + + void init_variant(Variant variant); + + void optimize_attach_point_lists(); + + template + void place(Color c, Move mv); + + void place_setup(const Setup& setup); + + void write_pieces_left(ostream& out, Color c, + const PiecesLeftList& pieces_left, unsigned begin, + unsigned end) const; + + void write_color_info_line1(ostream& out, Color c) const; + + void write_color_info_line2(ostream& out, Color c, + const PiecesLeftList& pieces_left) const; + + void write_color_info_line3(ostream& out, Color c, + const PiecesLeftList& pieces_left) const; + + void write_info_line(ostream& out, unsigned y, + const ColorMap& pieces_left) const; +}; + + +inline bool Board::find_move(const MovePoints& points, Move& mv) const +{ + return m_bc->find_move(points, mv); +} + +inline bool Board::find_move(const MovePoints& points, Piece piece, + Move& mv) const +{ + return m_bc->find_move(points, piece, mv); +} + +inline Move Board::from_string(const string& s) const +{ + return m_bc->from_string(s); +} + +inline unsigned Board::get_adj_status(Point p, Color c) const +{ + auto i = m_bc->get_adj_status_list(p).begin(); + unsigned result = is_forbidden(*i, c); // bool converted to integer is 1 + for (unsigned j = 1; j < PrecompMoves::adj_status_nu_adj; ++j) + result |= (is_forbidden(*(++i), c) << j); + return result; +} + +inline Color::IntType Board::get_alt_player() const +{ + LIBBOARDGAME_ASSERT(m_variant == Variant::classic_3); + return static_cast(get_nu_onboard_pieces(Color(3)) % 3); +} + +inline const PointList& Board::get_attach_points(Color c) const +{ + return m_attach_points[c]; +} + +inline BoardType Board::get_board_type() const +{ + return m_bc->get_board_type(); +} + +inline ColorMove Board::get_move(unsigned n) const +{ + return m_moves[n]; +} + +inline const MoveInfoExt2& Board::get_move_info_ext_2(Move mv) const +{ + LIBBOARDGAME_ASSERT(! mv.is_null()); + LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves()); + return *(m_move_info_ext_2_array + mv.to_int()); +} + +inline Piece Board::get_move_piece(Move mv) const +{ + return m_bc->get_move_piece(mv); +} + +inline Range Board::get_move_points(Move mv) const +{ + return m_bc->get_move_points(mv); +} + +inline auto Board::get_moves() const +-> const ArrayList& +{ + return m_moves; +} + +inline PrecompMoves::Range Board::get_moves(Piece piece, Point p, + unsigned adj_status) const +{ + return m_bc->get_moves(piece, p, adj_status); +} + +inline Color Board::get_next(Color c) const +{ + return c.get_next(m_nu_colors); +} + +inline Color::IntType Board::get_nu_colors() const +{ + return m_nu_colors; +} + +inline unsigned Board::get_nu_left_piece(Color c, Piece piece) const +{ + LIBBOARDGAME_ASSERT(piece.to_int() < get_nu_uniq_pieces()); + return m_state_color[c].nu_left_piece[piece]; +} + +inline unsigned Board::get_nu_moves() const +{ + return m_moves.size(); +} + +inline Color::IntType Board::get_nu_nonalt_colors() const +{ + return m_variant != Variant::classic_3 ? m_nu_colors : 3; +} + +inline unsigned Board::get_nu_onboard_pieces() const +{ + return m_state_base.nu_onboard_pieces_all; +} + +inline unsigned Board::get_nu_onboard_pieces(Color c) const +{ + return m_state_color[c].nu_onboard_pieces; +} + +inline unsigned Board::get_nu_players() const +{ + return m_nu_players; +} + +inline unsigned Board::get_nu_piece_instances(Piece piece) const +{ + return m_bc->get_piece_info(piece).get_nu_instances(); +} + +inline Piece::IntType Board::get_nu_uniq_pieces() const +{ + return m_bc->get_nu_pieces(); +} + +inline const PieceInfo& Board::get_piece_info(Piece piece) const +{ + return m_bc->get_piece_info(piece); +} + +inline bool Board::get_piece_by_name(const string& name, Piece& piece) const +{ + return m_bc->get_piece_by_name(name, piece); +} + +inline const Board::PiecesLeftList& Board::get_pieces_left(Color c) const +{ + return m_state_color[c].pieces_left; +} + +inline PointState Board::get_point_state(Point p) const +{ + return PointState(m_state_base.point_state[p].to_int()); +} + +inline const Board::PointStateGrid& Board::get_point_state() const +{ + return m_state_base.point_state; +} + +inline Color Board::get_previous(Color c) const +{ + return c.get_previous(m_nu_colors); +} + +inline ScoreType Board::get_score(Color c) const +{ + if (m_nu_colors == 2) + return get_score_twocolor(c); + else if (m_nu_players == 2) + return get_score_multicolor(c); + else + return get_score_multiplayer(c); +} + +inline ScoreType Board::get_score_twocolor(Color c) const +{ + LIBBOARDGAME_ASSERT(m_nu_colors == 2); + auto points0 = get_points(Color(0)); + auto points1 = get_points(Color(1)); + if (c == Color(0)) + return points0 - points1; + else + return points1 - points0; +} + +inline ScoreType Board::get_score_twoplayer(Color c) const +{ + LIBBOARDGAME_ASSERT(m_nu_players == 2); + if (m_nu_colors == 2) + return get_score_twocolor(c); + else + return get_score_multicolor(c); +} + +inline ScoreType Board::get_score_multicolor(Color c) const +{ + LIBBOARDGAME_ASSERT(m_nu_players == 2 && m_nu_colors == 4); + auto points0 = get_points(Color(0)) + get_points(Color(2)); + auto points1 = get_points(Color(1)) + get_points(Color(3)); + if (c == Color(0) || c == Color(2)) + return points0 - points1; + else + return points1 - points0; +} + +inline ScoreType Board::get_score_multiplayer(Color c) const +{ + LIBBOARDGAME_ASSERT(m_nu_players > 2); + ScoreType score = 0; + auto nu_players = static_cast(m_nu_players); + for (Color i : get_colors()) + if (i != c) + score -= get_points(i); + score = get_points(c) + score / (static_cast(nu_players) - 1); + return score; +} + +inline Color Board::get_second_color(Color c) const +{ + return m_second_color[c]; +} + +inline const Setup& Board::get_setup() const +{ + return m_setup; +} + +inline Color Board::get_starting_point_color(Point p) const +{ + return m_starting_points.get_starting_point_color(p); +} + +inline const ArrayList& + Board::get_starting_points(Color c) const +{ + return m_starting_points.get_starting_points(c); +} + +inline Color Board::get_to_play() const +{ + return m_state_base.to_play; +} + +inline const PieceTransforms& Board::get_transforms() const +{ + return m_bc->get_transforms(); +} + +inline Variant Board::get_variant() const +{ + return m_variant; +} + +inline void Board::init(const Setup* setup) +{ + init(m_variant, setup); +} + +inline bool Board::is_attach_point(Point p, Color c) const +{ + return m_state_color[c].is_attach_point[p]; +} + +inline bool Board::is_colored_starting_point(Point p) const +{ + return m_starting_points.is_colored_starting_point(p); +} + +inline bool Board::is_colorless_starting_point(Point p) const +{ + return m_starting_points.is_colorless_starting_point(p); +} + +inline bool Board::is_first_piece(Color c) const +{ + return m_state_color[c].nu_onboard_pieces == 0; +} + +inline bool Board::is_forbidden(Point p, Color c) const +{ + return m_state_color[c].forbidden[p]; +} + +inline const GridExt& Board::is_forbidden(Color c) const +{ + return m_state_color[c].forbidden; +} + +inline bool Board::is_forbidden(Color c, Move mv) const +{ + auto points = get_move_points(mv); + auto i = points.begin(); + auto end = points.end(); + do + if (m_state_color[c].forbidden[*i]) + return true; + while (++i != end); + return false; +} + +inline bool Board::is_legal(Move mv) const +{ + return is_legal(m_state_base.to_play, mv); +} + +inline bool Board::is_piece_left(Color c, Piece piece) const +{ + LIBBOARDGAME_ASSERT(piece.to_int() < get_nu_uniq_pieces()); + return m_state_color[c].nu_left_piece[piece] > 0; +} + +inline bool Board::is_same_player(Color c1, Color c2) const +{ + return c1 == c2 || c1 == m_second_color[c2]; +} + +template +inline void Board::place(Color c, Move mv) +{ + LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE); + LIBBOARDGAME_ASSERT(m_max_adj_attach == MAX_ADJ_ATTACH); + auto& info = BoardConst::get_move_info(mv, m_move_info_array); + auto& info_ext = BoardConst::get_move_info_ext( + mv, m_move_info_ext_array); + auto piece = info.get_piece(); + auto& state_color = m_state_color[c]; + LIBBOARDGAME_ASSERT(state_color.nu_left_piece[piece] > 0); + auto score_points = m_score_points[piece]; + if (--state_color.nu_left_piece[piece] == 0) + { + state_color.pieces_left.remove_fast(piece); + if (state_color.pieces_left.empty()) + { + state_color.points += m_bonus_all_pieces; + if (MAX_SIZE == 7) // Nexos + LIBBOARDGAME_ASSERT(m_bonus_one_piece == 0); + else if (score_points == 1) + state_color.points += m_bonus_one_piece; + } + } + ++m_state_base.nu_onboard_pieces_all; + ++state_color.nu_onboard_pieces; + state_color.points += score_points; + auto i = info.begin(); + auto end = info.end(); + do + { + m_state_base.point_state[*i] = PointState(c); + for_each_color([&](Color c) { + m_state_color[c].forbidden[*i] = true; + }); + } + while (++i != end); + if (MAX_SIZE == 7) // Nexos + { + LIBBOARDGAME_ASSERT(info_ext.size_adj_points == 0); + i = info_ext.begin_attach(); + end = i + info_ext.size_attach_points; + } + else + { + end = info_ext.end_adj(); + for (i = info_ext.begin_adj(); i != end; ++i) + state_color.forbidden[*i] = true; + LIBBOARDGAME_ASSERT(i == info_ext.begin_attach()); + end += info_ext.size_attach_points; + } + auto& attach_points = m_attach_points[c]; + auto n = attach_points.size(); + do + if (! state_color.forbidden[*i] && ! state_color.is_attach_point[*i]) + { + state_color.is_attach_point[*i] = true; + attach_points.get_unchecked(n) = *i; + ++n; + } + while (++i != end); + attach_points.resize(n); +} + +template +inline void Board::play(Color c, Move mv) +{ + place(c, mv); + m_moves.push_back(ColorMove(c, mv)); + m_state_base.to_play = get_next(c); +} + +inline void Board::play(ColorMove mv) +{ + play(mv.color, mv.move); +} + +inline void Board::restore_snapshot() +{ + LIBBOARDGAME_ASSERT(m_snapshot.moves_size <= m_moves.size()); + auto& geo = get_geometry(); + m_moves.resize(m_snapshot.moves_size); + m_state_base.to_play = m_snapshot.state_base.to_play; + m_state_base.nu_onboard_pieces_all = + m_snapshot.state_base.nu_onboard_pieces_all; + m_state_base.point_state.memcpy_from(m_snapshot.state_base.point_state, + geo); + for (Color c : get_colors()) + { + const auto& snapshot_state = m_snapshot.state_color[c]; + auto& state = m_state_color[c]; + state.forbidden.copy_from(snapshot_state.forbidden, geo); + state.is_attach_point.copy_from(snapshot_state.is_attach_point, geo); + state.pieces_left = snapshot_state.pieces_left; + state.nu_left_piece = snapshot_state.nu_left_piece; + state.nu_onboard_pieces = snapshot_state.nu_onboard_pieces; + state.points = snapshot_state.points; + m_attach_points[c].resize(m_snapshot.attach_points_size[c]); + } +} + +inline void Board::set_to_play(Color c) +{ + m_state_base.to_play = c; +} + +inline string Board::to_string(Move mv, bool with_piece_name) const +{ + return m_bc->to_string(mv, with_piece_name); +} + +//----------------------------------------------------------------------------- + +inline ostream& operator<<(ostream& out, const Board& bd) +{ + bd.write(out); + return out; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOARD_H diff --git a/src/libpentobi_base/BoardConst.cpp b/src/libpentobi_base/BoardConst.cpp new file mode 100644 index 0000000..3ca651b --- /dev/null +++ b/src/libpentobi_base/BoardConst.cpp @@ -0,0 +1,1128 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardConst.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "BoardConst.h" + +#include +#include "Marker.h" +#include "PieceTransformsClassic.h" +#include "PieceTransformsTrigon.h" +#include "libboardgame_base/Transform.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/StringUtil.h" + +namespace libpentobi_base { + +using libboardgame_base::Transform; +using libboardgame_util::split; +using libboardgame_util::to_lower; +using libboardgame_util::trim; + +//----------------------------------------------------------------------------- + +namespace { + +const bool log_move_creation = false; + +/** Local variable used during construction. + Making this variable global slightly speeds up construction and a + thread-safe construction is not needed. */ +Marker g_marker; + +/** Non-compact representation of lists of moves of a piece at a point + constrained by the forbidden status of adjacent points. + Only used during construction. See g_marker why this variable is global. */ +Grid, PrecompMoves::nu_adj_status>> + g_full_move_table; + + +bool is_reverse(MovePoints::const_iterator begin1, const Point* begin2, unsigned size) +{ + auto j = begin2 + size - 1; + for (auto i = begin1; i != begin1 + size; ++i, --j) + if (*i != *j) + return false; + return true; +} + +// Sort points using the ordering used in blksgf files (switches the direction +// of the y axis!) +void sort_piece_points(PiecePoints& points) +{ + auto check = [&](unsigned short a, unsigned short b) + { + if ((points[a].y == points[b].y && points[a].x > points[b].x) + || points[a].y < points[b].y) + swap(points[a], points[b]); + }; + // Minimal number of necessary comparisons with sorting networks + auto size = points.size(); + switch (size) + { + case 7: + check(1, 2); + check(3, 4); + check(5, 6); + check(0, 2); + check(3, 5); + check(4, 6); + check(0, 1); + check(4, 5); + check(2, 6); + check(0, 4); + check(1, 5); + check(0, 3); + check(2, 5); + check(1, 3); + check(2, 4); + check(2, 3); + break; + case 6: + check(1, 2); + check(4, 5); + check(0, 2); + check(3, 5); + check(0, 1); + check(3, 4); + check(2, 5); + check(0, 3); + check(1, 4); + check(2, 4); + check(1, 3); + check(2, 3); + break; + case 5: + check(0, 1); + check(3, 4); + check(2, 4); + check(2, 3); + check(1, 4); + check(0, 3); + check(0, 2); + check(1, 3); + check(1, 2); + break; + case 4: + check(0, 1); + check(2, 3); + check(0, 2); + check(1, 3); + check(1, 2); + break; + case 3: + check(1, 2); + check(0, 2); + check(0, 1); + break; + case 2: + check(0, 1); + break; + default: + LIBBOARDGAME_ASSERT(size == 1); + } +} + +vector create_pieces_callisto(const Geometry& geo, + PieceSet piece_set, + const PieceTransforms& transforms) +{ + vector pieces; + pieces.reserve(19); + pieces.emplace_back("1", + PiecePoints{ CoordPoint(0, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 3); + pieces.emplace_back("W", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1), + CoordPoint(0, 0), CoordPoint(0, 1), + CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("X", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1), + CoordPoint(0, 0), CoordPoint(0, 1), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("T5", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("U", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("L", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("T4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("Z", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("O", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0), CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("V", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("I", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("2", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + return pieces; +} + +vector create_pieces_classic(const Geometry& geo, + PieceSet piece_set, + const PieceTransforms& transforms) +{ + vector pieces; + // Define the 21 standard pieces. The piece names are the standard names as + // in http://blokusstrategy.com/?p=48. The default orientation is chosen + // such that it resembles the letter. + pieces.reserve(21); + pieces.emplace_back("V5", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2), CoordPoint(1, 0), + CoordPoint(2, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("L5", + PiecePoints{ CoordPoint(0, 1), CoordPoint(1, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("Z5", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("N", + PiecePoints{ CoordPoint(-1, 1), CoordPoint(-1, 0), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2)}, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("W", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1), + CoordPoint(0, 0), CoordPoint(0, 1), + CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("X", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1), + CoordPoint(0, 0), CoordPoint(0, 1), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("F", + PiecePoints{ CoordPoint(0, -1), CoordPoint(1, -1), + CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("I5", + PiecePoints{ CoordPoint(0, 2), CoordPoint(0, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("T5", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("Y", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(0, 1), + CoordPoint(0, 2) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("P", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(1, 0), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("U", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("L4", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("I4", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(0, 2) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("T4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("Z4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("O", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0), CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("V3", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("I3", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("2", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("1", + PiecePoints{ CoordPoint(0, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + return pieces; +} + +vector create_pieces_junior(const Geometry& geo, + PieceSet piece_set, + const PieceTransforms& transforms) +{ + vector pieces; + pieces.reserve(12); + pieces.emplace_back("L5", + PiecePoints{ CoordPoint(0, 1), CoordPoint(1, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("P", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(1, 0), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("I5", + PiecePoints{ CoordPoint(0, 2), CoordPoint(0, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("O", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0), CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("T4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("Z4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("L4", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("I4", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(0, -2) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("V3", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("I3", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("2", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("1", + PiecePoints{ CoordPoint(0, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + return pieces; +} + +vector create_pieces_trigon(const Geometry& geo, + PieceSet piece_set, + const PieceTransforms& transforms) +{ + vector pieces; + // Define the 22 standard Trigon pieces. The piece names are similar to one + // of the possible notations from the thread "Trigon book: how to play, how + // to win" from August 2010 in the Blokus forums + // http://forum.blokus.refreshed.be/viewtopic.php?f=2&t=2539#p9867 + // apart from that the smallest pieces are named '2' and '1' like in + // Classic to avoid to many pieces with letter 'I' and that numbers are + // only used if there is more than one piece with the same letter. + pieces.reserve(22); + pieces.emplace_back("I6", + PiecePoints{ CoordPoint(1, -1), CoordPoint(2, -1), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(-1, 1), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("L6", + PiecePoints{ CoordPoint(1, -1), CoordPoint(2, -1), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("V", + PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, -1), + CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(2, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("S", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, -1), + CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(-1, 1), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("P6", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(2, 0), + CoordPoint(-1, 1), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("F", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(1, 1), + CoordPoint(2, 1), CoordPoint(1, 2) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("W", + PiecePoints{ CoordPoint(1, -1), CoordPoint(-1, 0), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(2, 0), CoordPoint(3, 0) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("A6", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(2, 0), + CoordPoint(0, 1), CoordPoint(2, 1) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("G", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(0, 1), + CoordPoint(1, 1), CoordPoint(2, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("Y", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(-1, 0), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(-1, 1), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("X", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(-1, 1), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("O", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, -1), + CoordPoint(1, -1), CoordPoint(-1, 0), + CoordPoint(0, 0), CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("I5", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(-1, 1), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("L5", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(0, 1), + CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("C5", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(1, 1), + CoordPoint(2, 1) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("P5", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(2, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("I4", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(-1, 1), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("C4", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("A4", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(2, 0) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("I3", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("2", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("1", + PiecePoints{ CoordPoint(0, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + return pieces; +} + +vector create_pieces_nexos(const Geometry& geo, + PieceSet piece_set, + const PieceTransforms& transforms) +{ + vector pieces; + pieces.reserve(24); + pieces.emplace_back("I4", + PiecePoints{ CoordPoint(0, -3), CoordPoint(0, -2), + CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(0, 2), + CoordPoint(0, 3) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("L4", + PiecePoints{ CoordPoint(0, -3), CoordPoint(0, -2), + CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(1, 2) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("Y", + PiecePoints{ CoordPoint(0, -1), CoordPoint(-1, 0), + CoordPoint(0, 1), CoordPoint(0, 2), + CoordPoint(0, 3)}, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("N", + PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0), + CoordPoint(0, 1), CoordPoint(0, 2), + CoordPoint(0, 3)}, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("V4", + PiecePoints{ CoordPoint(-3, 0), CoordPoint(-2, 0), + CoordPoint(-1, 0), CoordPoint(0, -1), + CoordPoint(0, -2), CoordPoint(0, -3) }, + geo, transforms, piece_set, CoordPoint(-1, 0)); + pieces.emplace_back("W", + PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0), + CoordPoint(0, 1), CoordPoint(1, 2)}, + geo, transforms, piece_set, CoordPoint(-1, 0)); + pieces.emplace_back("Z4", + PiecePoints{ CoordPoint(-1, -2), CoordPoint(0, -1), + CoordPoint(0, 0), CoordPoint(0, 1), + CoordPoint(1, 2) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("T4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(0, 2), + CoordPoint(0, 3) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("E", + PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(-1, 2)}, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("U4", + PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(2, -1) }, + geo, transforms, piece_set, CoordPoint(-1, 0)); + pieces.emplace_back("X", + PiecePoints{ CoordPoint(0, -1), CoordPoint(-1, 0), + CoordPoint(1, 0), CoordPoint(0, 1)}, + geo, transforms, piece_set, CoordPoint(0, -1)); + pieces.emplace_back("F", + PiecePoints{ CoordPoint(1, -2), CoordPoint(0, -1), + CoordPoint(1, 0), CoordPoint(0, 1)}, + geo, transforms, piece_set, CoordPoint(0, -1)); + pieces.emplace_back("H", + PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(2, 1)}, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("J", + PiecePoints{ CoordPoint(0, -3), CoordPoint(0, -2), + CoordPoint(0, -1), CoordPoint(-1, 0), + CoordPoint(-2, -1) }, + geo, transforms, piece_set, CoordPoint(-1, 0)); + pieces.emplace_back("G", + PiecePoints{ CoordPoint(2, -1), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(1, 2)}, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("O", + PiecePoints{ CoordPoint(1, 0), CoordPoint(2, 1), + CoordPoint(0, 1), CoordPoint(1, 2)}, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("I3", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(0, 2), + CoordPoint(0, 3) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("L3", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(1, 2) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("T3", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(1, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("Z3", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 1), + CoordPoint(1, 2) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("U3", + PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0), + CoordPoint(2, -1) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("V2", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1) }, + geo, transforms, piece_set, CoordPoint(-1, 0)); + pieces.emplace_back("I2", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("1", + PiecePoints{ CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + return pieces; +} + +} // namespace + +//----------------------------------------------------------------------------- + +BoardConst::BoardConst(BoardType board_type, PieceSet piece_set) + : m_board_type(board_type), + m_piece_set(piece_set), + m_geo(libpentobi_base::get_geometry(board_type)) +{ + switch (board_type) + { + case BoardType::classic: + m_nu_moves = Move::onboard_moves_classic + 1; + break; + case BoardType::trigon: + m_nu_moves = Move::onboard_moves_trigon + 1; + break; + case BoardType::trigon_3: + m_nu_moves = Move::onboard_moves_trigon_3 + 1; + break; + case BoardType::duo: + if (piece_set == PieceSet::classic) + m_nu_moves = Move::onboard_moves_duo + 1; + else + { + LIBBOARDGAME_ASSERT(piece_set == PieceSet::junior); + m_nu_moves = Move::onboard_moves_junior + 1; + } + break; + case BoardType::nexos: + m_nu_moves = Move::onboard_moves_nexos + 1; + break; + case BoardType::callisto: + m_nu_moves = Move::onboard_moves_callisto + 1; + break; + case BoardType::callisto_2: + m_nu_moves = Move::onboard_moves_callisto_2 + 1; + break; + case BoardType::callisto_3: + m_nu_moves = Move::onboard_moves_callisto_3 + 1; + break; + } + switch (piece_set) + { + case PieceSet::classic: + m_transforms.reset(new PieceTransformsClassic); + m_pieces = create_pieces_classic(m_geo, piece_set, *m_transforms); + m_max_piece_size = 5; + m_max_adj_attach = 16; + m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<5>))); + m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<16>))); + break; + case PieceSet::junior: + m_transforms.reset(new PieceTransformsClassic); + m_pieces = create_pieces_junior(m_geo, piece_set, *m_transforms); + m_max_piece_size = 5; + m_max_adj_attach = 16; + m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<5>))); + m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<16>))); + break; + case PieceSet::trigon: + m_transforms.reset(new PieceTransformsTrigon); + m_pieces = create_pieces_trigon(m_geo, piece_set, *m_transforms); + m_max_piece_size = 6; + m_max_adj_attach = 22; + m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<6>))); + m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<22>))); + break; + case PieceSet::nexos: + m_transforms.reset(new PieceTransformsClassic); + m_pieces = create_pieces_nexos(m_geo, piece_set, *m_transforms); + m_max_piece_size = 7; + m_max_adj_attach = 12; + m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<7>))); + m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<12>))); + break; + case PieceSet::callisto: + m_transforms.reset(new PieceTransformsClassic); + m_pieces = create_pieces_callisto(m_geo, piece_set, *m_transforms); + m_max_piece_size = 5; + // m_max_adj_attach is actually 10 in Callisto, but we care more about + // the performance in the classic Blokus variants and some code is + // faster if we don't have to handle different values for + // m_max_adj_attach for the same m_max_piece_size. + m_max_adj_attach = 16; + m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<5>))); + m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<16>))); + break; + } + m_move_info_ext_2.reset(new MoveInfoExt2[m_nu_moves]); + m_nu_pieces = static_cast(m_pieces.size()); + init_adj_status(); + auto width = m_geo.get_width(); + auto height = m_geo.get_height(); + for (Point p : m_geo) + m_compare_val[p] = + (height - m_geo.get_y(p) - 1) * width + m_geo.get_x(p); + create_moves(); + switch (piece_set) + { + case PieceSet::classic: + LIBBOARDGAME_ASSERT(m_nu_pieces == 21); + break; + case PieceSet::junior: + LIBBOARDGAME_ASSERT(m_nu_pieces == 12); + break; + case PieceSet::trigon: + LIBBOARDGAME_ASSERT(m_nu_pieces == 22); + break; + case PieceSet::nexos: + LIBBOARDGAME_ASSERT(m_nu_pieces == 24); + break; + case PieceSet::callisto: + LIBBOARDGAME_ASSERT(m_nu_pieces == 12); + break; + } + if (board_type == BoardType::duo || board_type == BoardType::callisto_2) + init_symmetry_info<5>(); + else if (board_type == BoardType::trigon) + init_symmetry_info<6>(); +} + +template +inline void BoardConst::create_move(unsigned& moves_created, Piece piece, + const MovePoints& points, Point label_pos) +{ + LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE); + LIBBOARDGAME_ASSERT(m_max_adj_attach == MAX_ADJ_ATTACH); + LIBBOARDGAME_ASSERT(moves_created < m_nu_moves); + Move mv(static_cast(moves_created)); + void* place = + static_cast*>(m_move_info.get()) + + moves_created; + new(place) MoveInfo(piece, points); + place = + static_cast*>(m_move_info_ext.get()) + + moves_created; + auto& info_ext = *new(place) MoveInfoExt(); + auto& info_ext_2 = m_move_info_ext_2[moves_created]; + ++moves_created; + auto scored_points = &info_ext_2.scored_points[0]; + for (auto p : points) + if (m_board_type != BoardType::nexos || m_geo.get_point_type(p) != 0) + *(scored_points++) = p; + info_ext_2.scored_points_size = static_cast( + scored_points - &info_ext_2.scored_points[0]); + auto begin = info_ext_2.begin_scored_points(); + auto end = info_ext_2.end_scored_points(); + g_marker.clear(); + for (auto i = begin; i != end; ++i) + g_marker.set(*i); + for (auto i = begin; i != end; ++i) + { + auto j = m_adj_status_list[*i].begin(); + unsigned adj_status = g_marker[*j]; + for (unsigned k = 1; k < PrecompMoves::adj_status_nu_adj; ++k) + adj_status |= (g_marker[*(++j)] << k); + for (unsigned j = 0; j < PrecompMoves::nu_adj_status; ++j) + if ((j & adj_status) == 0) + g_full_move_table[*i][j].push_back(mv); + } + Point* p = info_ext.points; + for (auto i = begin; i != end; ++i) + for (Point j : m_geo.get_adj(*i)) + if (! g_marker[j]) + { + g_marker.set(j); + *(p++) = j; + } + info_ext.size_adj_points = static_cast(p - info_ext.points); + for (auto i = begin; i != end; ++i) + for (Point j : m_geo.get_diag(*i)) + if (! g_marker[j]) + { + g_marker.set(j); + *(p++) = j; + } + info_ext.size_attach_points = + static_cast(p - info_ext.end_adj()); + info_ext_2.label_pos = label_pos; + info_ext_2.breaks_symmetry = false; + info_ext_2.symmetric_move = Move::null(); + m_nu_attach_points[piece] = + max(m_nu_attach_points[piece], + static_cast(info_ext.size_attach_points)); + if (log_move_creation) + { + Grid grid; + grid.fill('.', m_geo); + for (auto i = begin; i != end; ++i) + grid[*i] = 'O'; + for (auto i = info_ext.begin_adj(); i != info_ext.end_adj(); ++i) + grid[*i] = '+'; + for (auto i = info_ext.begin_attach(); i != info_ext.end_attach(); ++i) + grid[*i] = '*'; + LIBBOARDGAME_LOG("Move ", mv.to_int(), ":\n", grid.to_string(m_geo)); + } +} + +void BoardConst::create_moves() +{ + // Unused move infos for Move::null() + LIBBOARDGAME_ASSERT(Move::null().to_int() == 0); + unsigned moves_created = 1; + + unsigned n = 0; + for (Piece::IntType i = 0; i < m_nu_pieces; ++i) + { + Piece piece(i); + if (m_max_piece_size == 5) + create_moves<5, 16>(moves_created, piece); + else if (m_max_piece_size == 6) + create_moves<6, 22>(moves_created, piece); + else + create_moves<7, 12>(moves_created, piece); + for (Point p : m_geo) + for (unsigned j = 0; j < PrecompMoves::nu_adj_status; ++j) + { + auto& list = g_full_move_table[p][j]; + m_precomp_moves.set_list_range(p, j, piece, n, + list.size()); + for (auto mv : list) + m_precomp_moves.set_move(n++, mv); + list.clear(); + } + } + LIBBOARDGAME_ASSERT(moves_created == m_nu_moves); + if (log_move_creation) + LIBBOARDGAME_LOG("Created moves: ", moves_created, ", precomp: ", n); +} + +template +void BoardConst::create_moves(unsigned& moves_created, Piece piece) +{ + auto& piece_info = m_pieces[piece.to_int()]; + if (log_move_creation) + LIBBOARDGAME_LOG("Creating moves for piece ", piece_info.get_name()); + auto& transforms = piece_info.get_transforms(); + auto nu_transforms = transforms.size(); + vector transformed_points(nu_transforms); + vector transformed_label_pos(nu_transforms); + for (size_t i = 0; i < nu_transforms; ++i) + { + auto transform = transforms[i]; + transformed_points[i] = piece_info.get_points(); + transform->transform(transformed_points[i].begin(), + transformed_points[i].end()); + sort_piece_points(transformed_points[i]); + transformed_label_pos[i] = + transform->get_transformed(piece_info.get_label_pos()); + } + auto piece_size = + static_cast(piece_info.get_points().size()); + MovePoints points; + for (MovePoints::IntType i = 0; i < MovePoints::max_size; ++i) + points.get_unchecked(i) = Point::null(); + points.resize(piece_size); + // Make outer loop iterator over geometry for better memory locality + for (Point p : m_geo) + { + if (log_move_creation) + LIBBOARDGAME_LOG("Creating moves at ", m_geo.to_string(p)); + auto x = m_geo.get_x(p); + auto y = m_geo.get_y(p); + auto point_type = m_geo.get_point_type(p); + for (size_t i = 0; i < nu_transforms; ++i) + { + if (log_move_creation) + { +#if ! LIBBOARDGAME_DISABLE_LOG + auto& transform = *transforms[i]; + LIBBOARDGAME_LOG("Transformation ", typeid(transform).name()); +#endif + } + if (transforms[i]->get_new_point_type() != point_type) + continue; + bool is_onboard = true; + for (MovePoints::IntType j = 0; j < piece_size; ++j) + { + auto& pp = transformed_points[i][j]; + int xx = pp.x + x; + int yy = pp.y + y; + if (! m_geo.is_onboard(CoordPoint(xx, yy))) + { + is_onboard = false; + break; + } + points[j] = m_geo.get_point(xx, yy); + } + if (! is_onboard) + continue; + CoordPoint label_pos = transformed_label_pos[i]; + label_pos.x += x; + label_pos.y += y; + create_move( + moves_created, piece, points, + m_geo.get_point(label_pos.x, label_pos.y)); + } + } +} + +Move BoardConst::from_string(const string& s) const +{ + string trimmed = to_lower(trim(s)); + if (trimmed == "null") + return Move::null(); + vector v = split(trimmed, ','); + if (v.size() > PieceInfo::max_size) + throw runtime_error("illegal move (too many points)"); + bool is_nexos = (m_board_type == BoardType::nexos); + MovePoints points; + for (const auto& s : v) + { + Point p; + if (! m_geo.from_string(s, p)) + throw runtime_error("illegal move (invalid point)"); + if (is_nexos) + { + auto point_type = m_geo.get_point_type(p); + if (point_type != 1 && point_type != 2) + // Silently discard points that are not line segments, such + // files were written by some (unreleased) versions of Pentobi. + continue; + } + points.push_back(p); + } + Move mv; + if (! find_move(points, mv)) + throw runtime_error("illegal move"); + return mv; +} + +const BoardConst& BoardConst::get(Variant variant) +{ + static map>> board_const; + auto board_type = libpentobi_base::get_board_type(variant); + auto piece_set = libpentobi_base::get_piece_set(variant); + auto& bc = board_const[board_type][piece_set]; + if (! bc) + bc.reset(new BoardConst(board_type, piece_set)); + return *bc; +} + +bool BoardConst::get_piece_by_name(const string& name, Piece& piece) const +{ + for (Piece::IntType i = 0; i < m_nu_pieces; ++i) + if (get_piece_info(Piece(i)).get_name() == name) + { + piece = Piece(i); + return true; + } + return false; +} + +bool BoardConst::find_move(const MovePoints& points, Move& move) const +{ + MovePoints sorted_points = points; + sort(sorted_points); + for (Piece::IntType i = 0; i < m_pieces.size(); ++i) + { + Piece piece(i); + for (auto mv : get_moves(piece, points[0])) + { + auto& info_ext_2 = get_move_info_ext_2(mv); + if (sorted_points.size() == info_ext_2.scored_points_size + && equal(sorted_points.begin(), sorted_points.end(), + info_ext_2.begin_scored_points())) + { + move = mv; + return true; + } + } + } + return false; +} + +bool BoardConst::find_move(const MovePoints& points, Piece piece, + Move& move) const +{ + MovePoints sorted_points = points; + sort(sorted_points); + for (auto mv : get_moves(piece, points[0])) + if (equal(sorted_points.begin(), sorted_points.end(), + get_move_points_begin(mv))) + { + move = mv; + return true; + } + return false; +} + +void BoardConst::init_adj_status() +{ + for (Point p : m_geo) + { + auto& l = m_adj_status_list[p]; + for (Point pp : m_geo.get_adj(p)) + { + if (l.size() == PrecompMoves::adj_status_nu_adj) + break; + l.push_back(pp); + } + for (Point pp : m_geo.get_diag(p)) + { + if (l.size() == PrecompMoves::adj_status_nu_adj) + break; + l.push_back(pp); + } + for (auto i = l.end(); i < l.begin() + PrecompMoves::adj_status_nu_adj; + ++i) + *i = Point::null(); + } +} + +template +void BoardConst::init_symmetry_info() +{ + m_symmetric_points.init(m_geo); + for (Move::IntType i = 1; i < m_nu_moves; ++i) + { + Move mv(i); + auto& info = get_move_info(mv); + auto& info_ext_2 = m_move_info_ext_2[i]; + info_ext_2.breaks_symmetry = false; + array sym_points; + MovePoints::IntType n = 0; + for (Point p : info) + { + auto symm_p = m_symmetric_points[p]; + auto end = info.end(); + if (find(info.begin(), end, symm_p) != end) + info_ext_2.breaks_symmetry = true; + sym_points[n++] = symm_p; + } + for (auto mv : get_moves(info.get_piece(), sym_points[0])) + if (is_reverse(sym_points.begin(), + get_move_info(mv).begin(), n)) + { + info_ext_2.symmetric_move = mv; + break; + } + } +} + +inline void BoardConst::sort(MovePoints& points) const +{ + auto check = [&](unsigned short a, unsigned short b) + { + if (m_compare_val[points[a]] > m_compare_val[points[b]]) + swap(points[a], points[b]); + }; + // Minimal number of necessary comparisons with sorting networks + auto size = points.size(); + switch (size) + { + case 7: + check(1, 2); + check(3, 4); + check(5, 6); + check(0, 2); + check(3, 5); + check(4, 6); + check(0, 1); + check(4, 5); + check(2, 6); + check(0, 4); + check(1, 5); + check(0, 3); + check(2, 5); + check(1, 3); + check(2, 4); + check(2, 3); + break; + case 6: + check(1, 2); + check(4, 5); + check(0, 2); + check(3, 5); + check(0, 1); + check(3, 4); + check(2, 5); + check(0, 3); + check(1, 4); + check(2, 4); + check(1, 3); + check(2, 3); + break; + case 5: + check(0, 1); + check(3, 4); + check(2, 4); + check(2, 3); + check(1, 4); + check(0, 3); + check(0, 2); + check(1, 3); + check(1, 2); + break; + case 4: + check(0, 1); + check(2, 3); + check(0, 2); + check(1, 3); + check(1, 2); + break; + case 3: + check(1, 2); + check(0, 2); + check(0, 1); + break; + case 2: + check(0, 1); + break; + default: + LIBBOARDGAME_ASSERT(size == 1); + } +} + +string BoardConst::to_string(Move mv, bool with_piece_name) const +{ + if (mv.is_null()) + return "null"; + auto& info_ext_2 = get_move_info_ext_2(mv); + ostringstream s; + if (with_piece_name) + s << '[' << get_piece_info(get_move_piece(mv)).get_name() << "]"; + bool is_first = true; + for (auto i = info_ext_2.begin_scored_points(); + i != info_ext_2.end_scored_points(); ++i) + { + if (! is_first) + s << ','; + else + is_first = false; + s << m_geo.to_string(*i); + } + return s.str(); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/BoardConst.h b/src/libpentobi_base/BoardConst.h new file mode 100644 index 0000000..fb15cdb --- /dev/null +++ b/src/libpentobi_base/BoardConst.h @@ -0,0 +1,363 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardConst.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_BOARD_CONST_H +#define LIBPENTOBI_BASE_BOARD_CONST_H + +#include "Geometry.h" +#include "MoveInfo.h" +#include "PieceInfo.h" +#include "PieceTransforms.h" +#include "PrecompMoves.h" +#include "SymmetricPoints.h" +#include "Variant.h" +#include "libboardgame_util/ArrayList.h" +#include "libboardgame_util/Range.h" + +namespace libpentobi_base { + +using namespace std; +using libboardgame_util::ArrayList; +using libboardgame_util::Range; + +//----------------------------------------------------------------------------- + +/** Constant precomputed data that is shared between all instances of Board + with a given board type and set of unique pieces per color. */ +class BoardConst +{ +public: + /** See get_adj_status_list() */ + typedef + ArrayList + AdjStatusList; + + /** Start of the MoveInfo array, which can be cached by the user in + performance-critical code and then passed into the static version of + get_move_info(). */ + typedef const void* MoveInfoArray; + + /** Start of the MoveInfoExt array, which can be cached by the user in + performance-critical code and then passed into the static version of + get_move_info_ext(). */ + typedef const void* MoveInfoExtArray; + + /** Get the single instance for a given board size. + The instance is created the first time this function is called. + This function is not thread-safe. */ + static const BoardConst& get(Variant variant); + + template + static const MoveInfo& + get_move_info(Move mv, MoveInfoArray move_info_array); + + template + static const MoveInfoExt& + get_move_info_ext(Move mv, MoveInfoExtArray move_info_ext_array); + + + Piece::IntType get_nu_pieces() const; + + const PieceInfo& get_piece_info(Piece piece) const; + + unsigned get_nu_attach_points(Piece piece) const; + + bool get_piece_by_name(const string& name, Piece& piece) const; + + const PieceTransforms& get_transforms() const; + + unsigned get_max_piece_size() const { return m_max_piece_size; } + + unsigned get_max_adj_attach() const { return m_max_adj_attach; } + + Range get_move_points(Move mv) const; + + /** Return start of move points array. + For unrolling loops, there are guaranteed to be as many elements + as the maximum piece size in the current game variant. If the piece + is smaller, the remaining points are guaranteed to be Point::null(). */ + const Point* get_move_points_begin(Move mv) const; + + template + const Point* get_move_points_begin(Move mv) const; + + Piece get_move_piece(Move mv) const; + + template + Piece get_move_piece(Move mv) const; + + MoveInfoArray get_move_info_array() const { return m_move_info.get(); } + + /** Get pointer to extended move info array. + Can be used to speed up the access to the move info by avoiding the + multiple pointer dereferencing of Board::get_move_info_ext(Move) */ + MoveInfoExtArray get_move_info_ext_array() const; + + const MoveInfoExt2& get_move_info_ext_2(Move mv) const; + + const MoveInfoExt2* get_move_info_ext_2_array() const; + + unsigned get_nu_moves() const; + + bool find_move(const MovePoints& points, Move& move) const; + + bool find_move(const MovePoints& points, Piece piece, Move& move) const; + + /** Get all moves of a piece at a point constrained by the forbidden + status of adjacent points. */ + PrecompMoves::Range get_moves(Piece piece, Point p, + unsigned adj_status = 0) const + { + return m_precomp_moves.get_moves(piece, p, adj_status); + } + + const PrecompMoves& get_precomp_moves() const { return m_precomp_moves; } + + BoardType get_board_type() const { return m_board_type; }; + + PieceSet get_piece_set() const { return m_piece_set; } + + const Geometry& get_geometry() const; + + /** List containing the points used for the adjacent status. + Contains the first PrecompMoves::adj_status_nu_adj points of + Geometry::get_adj() concatenated with Geometry::get_diag(). + Elements above end() may be accessed and contain Point::null() + for easy unrolling of loops. */ + const AdjStatusList& get_adj_status_list(Point p) const + { + return m_adj_status_list[p]; + } + + /** Only initialized in game variants with central symmetry of board + including startign points. */ + const SymmetricPoints& get_symmetrc_points() const + { + return m_symmetric_points; + } + + /** Convert a move to its string representation. + The string representation is a comma-separated list of points (without + spaces between the commas or points). If with_piece_name is true, + it is prepended by the piece name in square brackets (also without any + spaces). The representation without the piece name is used by the SGF + files and GTP interface used by Pentobi (version >= 0.2). */ + string to_string(Move mv, bool with_piece_name = false) const; + + Move from_string(const string& s) const; + + /** Sort move points using the ordering used in blksgf files. */ + void sort(MovePoints& points) const; + +private: + struct MallocFree + { + void operator()(void* x) { free(x); } + }; + + + Piece::IntType m_nu_pieces; + + unsigned m_nu_moves; + + unsigned m_max_piece_size; + + /** See MoveInfoExt */ + unsigned m_max_adj_attach; + + BoardType m_board_type; + + PieceSet m_piece_set; + + const Geometry& m_geo; + + vector m_pieces; + + Grid m_adj_status_list; + + unique_ptr m_transforms; + + PieceMap m_nu_attach_points{0}; + + /** Array of MoveInfo with MAX_SIZE being the maximum piece size + in the corresponding game variant. + See comments at MoveInfo. */ + unique_ptr m_move_info; + + /** Array of MoveInfoExt with MAX_ADJ_ATTACH being the + maximum total number of attach points and adjacent points of a piece in + the corresponding game variant. + See comments at MoveInfoExt. */ + unique_ptr m_move_info_ext; + + unique_ptr m_move_info_ext_2; + + PrecompMoves m_precomp_moves; + + /** Value for comparing points using the ordering used in blksgf files. + As specified in doc/blksgf/Pentobi-SGF.html, the order should be + (a1, b1, ..., a2, b2, ...) with y going upwards whereas the convention + for Point is that y goes downwards. */ + Grid m_compare_val; + + SymmetricPoints m_symmetric_points; + + + BoardConst(BoardType board_type, PieceSet piece_set); + + template + void create_move(unsigned& moves_created, Piece piece, + const MovePoints& points, Point label_pos); + + void create_moves(); + + template + void create_moves(unsigned& moves_created, Piece piece); + + template + const MoveInfo& get_move_info(Move mv) const; + + void init_adj_status(); + + template + void init_symmetry_info(); +}; + +inline const Geometry& BoardConst::get_geometry() const +{ + return m_geo; +} + +template +inline const MoveInfo& +BoardConst::get_move_info(Move mv, MoveInfoArray move_info_array) +{ + LIBBOARDGAME_ASSERT(! mv.is_null()); + return *(static_cast*>(move_info_array) + + mv.to_int()); +} + +template +inline const MoveInfo& BoardConst::get_move_info(Move mv) const +{ + LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE); + return get_move_info(mv, m_move_info.get()); +} + +template +inline const MoveInfoExt& +BoardConst::get_move_info_ext(Move mv, MoveInfoExtArray move_info_ext_array) +{ + LIBBOARDGAME_ASSERT(! mv.is_null()); + return *(static_cast*>( + move_info_ext_array) + mv.to_int()); +} + +inline const MoveInfoExt2& BoardConst::get_move_info_ext_2(Move mv) const +{ + LIBBOARDGAME_ASSERT(mv.to_int() < m_nu_moves); + return m_move_info_ext_2[mv.to_int()]; +} + +inline auto BoardConst::get_move_info_ext_array() const -> MoveInfoExtArray +{ + return m_move_info_ext.get(); +} + +inline const MoveInfoExt2* BoardConst::get_move_info_ext_2_array() const +{ + return m_move_info_ext_2.get(); +} + +template +inline Piece BoardConst::get_move_piece(Move mv) const +{ + return get_move_info(mv).get_piece(); +} + +inline Piece BoardConst::get_move_piece(Move mv) const +{ + if (m_max_piece_size == 5) + return get_move_piece<5>(mv); + else if (m_max_piece_size == 6) + return get_move_piece<6>(mv); + else + { + LIBBOARDGAME_ASSERT(m_max_piece_size == 7); + return get_move_piece<7>(mv); + } +} + +inline Range BoardConst::get_move_points(Move mv) const +{ + if (m_max_piece_size == 5) + { + auto& info = get_move_info<5>(mv); + return Range(info.begin(), info.end()); + } + else if (m_max_piece_size == 6) + { + auto& info = get_move_info<6>(mv); + return Range(info.begin(), info.end()); + } + else + { + LIBBOARDGAME_ASSERT(m_max_piece_size == 7); + auto& info = get_move_info<7>(mv); + return Range(info.begin(), info.end()); + } +} + +inline const Point* BoardConst::get_move_points_begin(Move mv) const +{ + if (m_max_piece_size == 5) + return get_move_points_begin<5>(mv); + else if (m_max_piece_size == 6) + return get_move_points_begin<6>(mv); + else + { + LIBBOARDGAME_ASSERT(m_max_piece_size == 7); + return get_move_points_begin<7>(mv); + } +} + +template +inline const Point* BoardConst::get_move_points_begin(Move mv) const +{ + return get_move_info(mv).begin(); +} + +inline unsigned BoardConst::get_nu_moves() const +{ + return m_nu_moves; +} + +inline unsigned BoardConst::get_nu_attach_points(Piece piece) const +{ + return m_nu_attach_points[piece]; +} + +inline Piece::IntType BoardConst::get_nu_pieces() const +{ + return m_nu_pieces; +} + +inline const PieceInfo& BoardConst::get_piece_info(Piece piece) const +{ + LIBBOARDGAME_ASSERT(piece.to_int() < m_pieces.size()); + return m_pieces[piece.to_int()]; +} + +inline const PieceTransforms& BoardConst::get_transforms() const +{ + return *m_transforms; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOARD_CONST_H diff --git a/src/libpentobi_base/BoardUpdater.cpp b/src/libpentobi_base/BoardUpdater.cpp new file mode 100644 index 0000000..2a275b6 --- /dev/null +++ b/src/libpentobi_base/BoardUpdater.cpp @@ -0,0 +1,154 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardUpdater.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "BoardUpdater.h" + +#include +#include "BoardUtil.h" +#include "NodeUtil.h" +#include "libboardgame_sgf/SgfUtil.h" + +namespace libpentobi_base { + +using libboardgame_sgf::InvalidTree; +using libboardgame_sgf::util::get_path_from_root; +using libpentobi_base::boardutil::get_current_position_as_setup; + +//----------------------------------------------------------------------------- + +namespace { + +/** List to hold remaining pieces of a color with one entry for each instance + of the same piece. */ +typedef ArrayList +AllPiecesLeftList; + +/** Helper function used in init_setup. */ +void handle_setup_property(const SgfNode& node, const char* id, Color c, + const Board& bd, Setup& setup, + ColorMap& pieces_left) +{ + if (! node.has_property(id)) + return; + vector values = node.get_multi_property(id); + for (const string& s : values) + { + Move mv; + try + { + mv = bd.from_string(s); + } + catch (const runtime_error& e) + { + throw InvalidTree(e.what()); + } + Piece piece = bd.get_move_piece(mv); + if (! pieces_left[c].remove(piece)) + throw InvalidTree("piece played twice"); + setup.placements[c].push_back(mv); + } +} + +/** Helper function used in init_setup. */ +void handle_setup_empty(const SgfNode& node, const Board& bd, Setup& setup, + ColorMap& pieces_left) +{ + if (! node.has_property("AE")) + return; + vector values = node.get_multi_property("AE"); + for (const string& s : values) + { + Move mv; + try + { + mv = bd.from_string(s); + } + catch (const runtime_error& e) + { + throw InvalidTree(e.what()); + } + for (Color c : bd.get_colors()) + { + if (setup.placements[c].remove(mv)) + { + Piece piece = bd.get_move_piece(mv); + LIBBOARDGAME_ASSERT(! pieces_left[c].contains(piece)); + pieces_left[c].push_back(piece); + break; + } + throw InvalidTree("invalid value for AE property"); + } + } +} + +/** Initialize the board with a new setup position. + Class Board only supports setup positions before any moves are played. To + support setup properties in any node, we create a new setup position from + the current position and the setup properties from the node and initialize + the board with it. */ +void init_setup(Board& bd, const SgfNode& node) +{ + Setup setup; + get_current_position_as_setup(bd, setup); + ColorMap all_pieces_left; + for (Color c : bd.get_colors()) + for (Piece piece : bd.get_pieces_left(c)) + for (unsigned i = 0; i < bd.get_nu_piece_instances(piece); ++i) + all_pieces_left[c].push_back(piece); + handle_setup_property(node, "A1", Color(0), bd, setup, all_pieces_left); + handle_setup_property(node, "A2", Color(1), bd, setup, all_pieces_left); + handle_setup_property(node, "A3", Color(2), bd, setup, all_pieces_left); + handle_setup_property(node, "A4", Color(3), bd, setup, all_pieces_left); + // AB, AW are equivalent to A1, A2 but only used in games with two colors + handle_setup_property(node, "AB", Color(0), bd, setup, all_pieces_left); + handle_setup_property(node, "AW", Color(1), bd, setup, all_pieces_left); + handle_setup_empty(node, bd, setup, all_pieces_left); + Color to_play; + if (! libpentobi_base::node_util::get_player(node, setup.to_play)) + { + // Try to guess who should be to play based on the setup pieces. + setup.to_play = Color(0); + for (Color c : bd.get_colors()) + if (setup.placements[c].size() < setup.placements[Color(0)].size()) + { + setup.to_play = c; + break; + } + } + bd.init(&setup); +} + +} // namespace + +//----------------------------------------------------------------------------- + +void BoardUpdater::update(Board& bd, const PentobiTree& tree, + const SgfNode& node) +{ + LIBBOARDGAME_ASSERT(tree.contains(node)); + bd.init(); + get_path_from_root(node, m_path); + for (const auto i : m_path) + { + if (libpentobi_base::node_util::has_setup(*i)) + init_setup(bd, *i); + auto mv = tree.get_move(*i); + if (! mv.is_null()) + { + if (! bd.is_piece_left(mv.color, bd.get_move_piece(mv.move))) + throw InvalidTree("piece played twice"); + bd.play(mv); + } + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/BoardUpdater.h b/src/libpentobi_base/BoardUpdater.h new file mode 100644 index 0000000..1925313 --- /dev/null +++ b/src/libpentobi_base/BoardUpdater.h @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardUpdater.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_BOARD_UPDATER_H +#define LIBPENTOBI_BASE_BOARD_UPDATER_H + +#include "Board.h" +#include "PentobiTree.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Updates a board state to a node in a game tree. */ +class BoardUpdater +{ +public: + /** Update the board to a node. + @throws Exception if tree contains invalid properties, moves that play + the same piece twice or other conditions that prevent the updater to + update the board to the given node. */ + void update(Board& bd, const PentobiTree& tree, const SgfNode& node); + +private: + /** Local variable reused for efficiency. */ + vector m_path; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOARD_UPDATER_H diff --git a/src/libpentobi_base/BoardUtil.cpp b/src/libpentobi_base/BoardUtil.cpp new file mode 100644 index 0000000..7e819a6 --- /dev/null +++ b/src/libpentobi_base/BoardUtil.cpp @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "BoardUtil.h" + +#include "PentobiSgfUtil.h" +#if LIBBOARDGAME_DEBUG +#include +#endif + +namespace libpentobi_base { +namespace boardutil { + +using namespace std; +using sgf_util::get_color_id; +using sgf_util::get_setup_id; + +//----------------------------------------------------------------------------- + +#if LIBBOARDGAME_DEBUG +string dump(const Board& bd) +{ + ostringstream s; + auto variant = bd.get_variant(); + Writer writer(s); + writer.begin_tree(); + writer.begin_node(); + writer.write_property("GM", to_string(variant)); + write_setup(writer, variant, bd.get_setup()); + writer.end_node(); + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + writer.begin_node(); + auto mv = bd.get_move(i); + auto id = get_color_id(variant, mv.color); + if (! mv.is_null()) + writer.write_property(id, bd.to_string(mv.move, false)); + writer.end_node(); + } + writer.end_tree(); + return s.str(); +} +#endif + +void get_current_position_as_setup(const Board& bd, Setup& setup) +{ + setup = bd.get_setup(); + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + auto mv = bd.get_move(i); + setup.placements[mv.color].push_back(mv.move); + } + setup.to_play = bd.get_to_play(); +} + +Move get_transformed(const Board& bd, Move mv, + const PointTransform& transform) +{ + auto& geo = bd.get_geometry(); + MovePoints points; + for (auto p : bd.get_move_points(mv)) + points.push_back(transform.get_transformed(p, geo)); + Move transformed_mv; + bd.find_move(points, bd.get_move_piece(mv), transformed_mv); + return transformed_mv; +} + +void write_setup(Writer& writer, Variant variant, const Setup& setup) +{ + auto& board_const = BoardConst::get(variant); + for (Color c : get_colors(variant)) + if (! setup.placements[c].empty()) + { + vector values; + for (Move mv : setup.placements[c]) + values.push_back(board_const.to_string(mv, false)); + writer.write_property(get_setup_id(variant, c), values); + } +} + +//----------------------------------------------------------------------------- + +} // namespace boardutil +} // namespace libpentobi_base diff --git a/src/libpentobi_base/BoardUtil.h b/src/libpentobi_base/BoardUtil.h new file mode 100644 index 0000000..e99717a --- /dev/null +++ b/src/libpentobi_base/BoardUtil.h @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_BOARDUTIL_H +#define LIBPENTOBI_BASE_BOARDUTIL_H + +#include "Board.h" +#include "libboardgame_sgf/Writer.h" + +namespace libpentobi_base { +namespace boardutil { + +using libboardgame_sgf::Writer; + +//----------------------------------------------------------------------------- + +#if LIBBOARDGAME_DEBUG +string dump(const Board& bd); +#endif + +/** Return the current position as setup. + Merges all placements from Board::get_setup() and played moved into a + single setup and sets the setup color to play to the current color to + play. */ +void get_current_position_as_setup(const Board& bd, Setup& setup); + +void write_setup(Writer& writer, Variant variant, const Setup& setup); + +Move get_transformed(const Board& bd, Move mv, + const PointTransform& transform); + +//----------------------------------------------------------------------------- + +} // namespace boardutil +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOARDUTIL_H diff --git a/src/libpentobi_base/Book.cpp b/src/libpentobi_base/Book.cpp new file mode 100644 index 0000000..58c9078 --- /dev/null +++ b/src/libpentobi_base/Book.cpp @@ -0,0 +1,144 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Book.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Book.h" + +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_util/Log.h" +#include "libpentobi_base/BoardUtil.h" + +//----------------------------------------------------------------------------- + +namespace libpentobi_base { + +using libboardgame_base::PointTransfIdent; +using libboardgame_base::PointTransfRefl; +using libboardgame_base::PointTransfReflRot180; +using libboardgame_base::PointTransfRot180; +using libboardgame_base::PointTransfRot270Refl; +using libboardgame_base::PointTransfTrigonReflRot60; +using libboardgame_base::PointTransfTrigonReflRot120; +using libboardgame_base::PointTransfTrigonReflRot240; +using libboardgame_base::PointTransfTrigonReflRot300; +using libboardgame_base::PointTransfTrigonRot60; +using libboardgame_base::PointTransfTrigonRot120; +using libboardgame_base::PointTransfTrigonRot240; +using libboardgame_base::PointTransfTrigonRot300; +using libboardgame_sgf::InvalidPropertyValue; +using libboardgame_sgf::TreeReader; +using boardutil::get_transformed; + +//----------------------------------------------------------------------------- + +Book::Book(Variant variant) + : m_tree(variant) +{ + get_transforms(variant, m_transforms, m_inv_transforms); +} + +Book::~Book() +{ +} + +Move Book::genmove(const Board& bd, Color c) +{ + if (bd.has_setup()) + // Book cannot handle setup positions + return Move::null(); + Move mv; + for (unsigned i = 0; i < m_transforms.size(); ++i) + if (genmove(bd, c, mv, *m_transforms[i], *m_inv_transforms[i])) + return mv; + return Move::null(); +} + +bool Book::genmove(const Board& bd, Color c, Move& mv, + const PointTransform& transform, + const PointTransform& inv_transform) +{ + LIBBOARDGAME_ASSERT(! bd.has_setup()); + auto node = &m_tree.get_root(); + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + ColorMove color_mv = bd.get_move(i); + color_mv.move = get_transformed(bd, color_mv.move, transform); + node = m_tree.find_child_with_move(*node, color_mv); + if (! node) + return false; + } + node = select_child(bd, c, m_tree, *node, inv_transform); + if (! node) + return false; + mv = get_transformed(bd, m_tree.get_move(*node).move, inv_transform); + return true; +} + +void Book::load(istream& in) +{ + TreeReader reader; + try + { + reader.read(in); + } + catch (const TreeReader::ReadError& e) + { + throw runtime_error(string("could not read book: ") + e.what()); + } + unique_ptr root = reader.get_tree_transfer_ownership(); + m_tree.init(root); + get_transforms(m_tree.get_variant(), m_transforms, m_inv_transforms); +} + +const SgfNode* Book::select_child(const Board& bd, Color c, + const PentobiTree& tree, const SgfNode& node, + const PointTransform& inv_transform) +{ + unsigned nu_children = node.get_nu_children(); + if (nu_children == 0) + return nullptr; + vector good_moves; + for (unsigned i = 0; i < nu_children; ++i) + { + auto& child = node.get_child(i); + ColorMove color_mv = tree.get_move(child); + if (color_mv.is_null()) + { + LIBBOARDGAME_LOG("WARNING: Book contains nodes without moves"); + continue; + } + if (color_mv.color != c) + { + LIBBOARDGAME_LOG("WARNING: Book contains non-alternating move sequences"); + continue; + } + auto mv = get_transformed(bd, color_mv.move, inv_transform); + if (! bd.is_legal(color_mv.color, mv)) + { + LIBBOARDGAME_LOG("WARNING: Book contains illegal move"); + continue; + } + if (m_tree.get_good_move(child) > 0) + { + LIBBOARDGAME_LOG(bd.to_string(mv), " !"); + good_moves.push_back(&child); + } + else + LIBBOARDGAME_LOG(bd.to_string(mv)); + } + if (good_moves.empty()) + return nullptr; + LIBBOARDGAME_LOG("Book moves: ", good_moves.size()); + unsigned nu_good_moves = static_cast(good_moves.size()); + return good_moves[m_random.generate() % nu_good_moves]; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Book.h b/src/libpentobi_base/Book.h new file mode 100644 index 0000000..0ab77ea --- /dev/null +++ b/src/libpentobi_base/Book.h @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Book.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_BOOK_H +#define LIBPENTOBI_BASE_BOOK_H + +#include +#include "Board.h" +#include "PentobiTree.h" +#include "libboardgame_base/PointTransform.h" +#include "libboardgame_util/RandomGenerator.h" + +namespace libpentobi_base { + +using libboardgame_util::RandomGenerator; + +//----------------------------------------------------------------------------- + +/** Opening book. + Opening books are stored as trees in SGF files. Thay contain move + annotation properties according to the SGF standard. The book will select + randomly among the child nodes that have the move annotation good move + or very good move (TE[1] or TE[2]). */ +class Book +{ +public: + explicit Book(Variant variant); + + ~Book(); + + void load(istream& in); + + Move genmove(const Board& bd, Color c); + + const PentobiTree& get_tree() const; + +private: + typedef libboardgame_base::PointTransform PointTransform; + + PentobiTree m_tree; + + RandomGenerator m_random; + + vector> m_transforms; + + vector> m_inv_transforms; + + bool genmove(const Board& bd, Color c, Move& mv, + const PointTransform& transform, + const PointTransform& inv_transform); + + const SgfNode* select_child(const Board& bd, Color c, + const PentobiTree& tree, const SgfNode& node, + const PointTransform& inv_transform); +}; + +inline const PentobiTree& Book::get_tree() const +{ + return m_tree; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOOK_H diff --git a/src/libpentobi_base/CMakeLists.txt b/src/libpentobi_base/CMakeLists.txt new file mode 100644 index 0000000..24ad32f --- /dev/null +++ b/src/libpentobi_base/CMakeLists.txt @@ -0,0 +1,78 @@ +set(pentobi_base_STAT_SRCS + BoardConst.h + BoardConst.cpp + Board.h + Board.cpp + BoardUpdater.h + BoardUpdater.cpp + BoardUtil.h + BoardUtil.cpp + Book.h + Book.cpp + CallistoGeometry.h + CallistoGeometry.cpp + Color.h + Color.cpp + ColorMap.h + ColorMove.h + Game.h + Game.cpp + Geometry.h + Grid.h + Marker.h + Move.h + MoveInfo.h + MoveList.h + MoveMarker.h + MovePoints.h + NexosGeometry.h + NexosGeometry.cpp + NodeUtil.h + NodeUtil.cpp + PentobiSgfUtil.h + PentobiSgfUtil.cpp + PentobiTree.h + PentobiTree.cpp + PentobiTreeWriter.h + PentobiTreeWriter.cpp + Piece.h + PieceInfo.h + PieceInfo.cpp + PieceMap.h + PieceTransformsClassic.h + PieceTransformsClassic.cpp + PieceTransformsClassic.h + PieceTransforms.cpp + PieceTransformsTrigon.h + PieceTransformsTrigon.cpp + PlayerBase.h + PlayerBase.cpp + Point.h + PointList.h + PointState.h + PointState.cpp + PrecompMoves.h + ScoreUtil.h + Setup.h + StartingPoints.h + StartingPoints.cpp + SymmetricPoints.h + SymmetricPoints.cpp + TreeUtil.h + TreeUtil.cpp + TrigonGeometry.h + TrigonGeometry.cpp + TrigonTransform.h + TrigonTransform.cpp + Variant.h + Variant.cpp +) + +if (PENTOBI_BUILD_GTP) + set(pentobi_base_STAT_SRCS ${pentobi_base_STAT_SRCS} + Engine.cpp + Engine.h + ) +endif() + +add_library(pentobi_base STATIC ${pentobi_base_STAT_SRCS}) diff --git a/src/libpentobi_base/CallistoGeometry.cpp b/src/libpentobi_base/CallistoGeometry.cpp new file mode 100644 index 0000000..68dc38c --- /dev/null +++ b/src/libpentobi_base/CallistoGeometry.cpp @@ -0,0 +1,125 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/CallistoGeometry.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "CallistoGeometry.h" + +#include "libboardgame_util/Unused.h" + +namespace libpentobi_base { + +using libboardgame_base::CoordPoint; + +//----------------------------------------------------------------------------- + +namespace { + +unsigned get_size_callisto(unsigned nu_players) +{ + if (nu_players == 2) + return 16; + LIBBOARDGAME_ASSERT(nu_players == 3 || nu_players == 4); + return 20; +} + +unsigned get_edge_callisto(unsigned nu_players) +{ + if (nu_players == 4) + return 6; + LIBBOARDGAME_ASSERT(nu_players == 2 || nu_players == 3); + return 2; +} + +bool is_onboard_callisto(unsigned x, unsigned y, unsigned width, + unsigned height, unsigned edge) +{ + unsigned dy = min(y, height - y - 1); + unsigned min_x = (width - edge) / 2 > dy ? (width - edge) / 2 - dy : 0; + unsigned max_x = width - min_x - 1; + return x >= min_x && x <= max_x; +} + +} // namespace + +//----------------------------------------------------------------------------- + +map> CallistoGeometry::s_geometry; + +CallistoGeometry::CallistoGeometry(unsigned nu_players) +{ + unsigned sz = get_size_callisto(nu_players); + m_edge = get_edge_callisto(nu_players); + Geometry::init(sz, sz); +} + +const CallistoGeometry& CallistoGeometry::get(unsigned nu_players) +{ + auto pos = s_geometry.find(nu_players); + if (pos != s_geometry.end()) + return *pos->second; + shared_ptr geometry(new CallistoGeometry(nu_players)); + return *s_geometry.insert(make_pair(nu_players, geometry)).first->second; +} + +auto CallistoGeometry::get_adj_coord(int x, int y) const -> AdjCoordList +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return AdjCoordList(); +} + +auto CallistoGeometry::get_diag_coord(int x, int y) const -> DiagCoordList +{ + DiagCoordList l; + l.push_back(CoordPoint(x, y - 1)); + l.push_back(CoordPoint(x - 1, y)); + l.push_back(CoordPoint(x + 1, y)); + l.push_back(CoordPoint(x, y + 1)); + return l; +} + +unsigned CallistoGeometry::get_period_x() const +{ + return 1; +} + +unsigned CallistoGeometry::get_period_y() const +{ + return 1; +} + +unsigned CallistoGeometry::get_point_type(int x, int y) const +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return 0; +} + +bool CallistoGeometry::init_is_onboard(unsigned x, unsigned y) const +{ + return is_onboard_callisto(x, y, get_width(), get_height(), m_edge); +} + +bool CallistoGeometry::is_center_section(unsigned x, unsigned y, + unsigned nu_players) +{ + auto size = get_size_callisto(nu_players); + if (x < size / 2 - 3 || y < size / 2 - 3) + return false; + x -= size / 2 - 3; + y -= size / 2 - 3; + if (x > 5 || y > 5) + return false; + return is_onboard_callisto(x, y, 6, 6, 2); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + diff --git a/src/libpentobi_base/CallistoGeometry.h b/src/libpentobi_base/CallistoGeometry.h new file mode 100644 index 0000000..f53cf8d --- /dev/null +++ b/src/libpentobi_base/CallistoGeometry.h @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/CallistoGeometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H +#define LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H + +#include +#include +#include "Geometry.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Geometry for the board game Callisto. + To fit in with the assumptions of the Blokus engine, points are "diagonal" + to each other if they are actually adjacent on the real board and the + "adjacent" relationship is not used. */ +class CallistoGeometry final + : public Geometry +{ +public: + /** Create or reuse an already created geometry. + @param nu_players The number of players (2, 3, or 4). */ + static const CallistoGeometry& get(unsigned nu_players); + + static bool is_center_section(unsigned x, unsigned y, unsigned nu_players); + + + AdjCoordList get_adj_coord(int x, int y) const override; + + DiagCoordList get_diag_coord(int x, int y) const override; + + unsigned get_point_type(int x, int y) const override; + + unsigned get_period_x() const override; + + unsigned get_period_y() const override; + +protected: + bool init_is_onboard(unsigned x, unsigned y) const override; + +private: + /** Stores already created geometries by number of players. */ + static map> s_geometry; + + + unsigned m_edge; + + + explicit CallistoGeometry(unsigned nu_players); +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H diff --git a/src/libpentobi_base/Color.cpp b/src/libpentobi_base/Color.cpp new file mode 100644 index 0000000..e5869a9 --- /dev/null +++ b/src/libpentobi_base/Color.cpp @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Color.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Color.h" + +#include +#include "libboardgame_util/StringUtil.h" + +namespace libpentobi_base { + +using libboardgame_util::to_lower; + +//----------------------------------------------------------------------------- + +Color::Color(const string& s) +{ + istringstream in(s); + in >> *this; + if (! in) + throw InvalidString("Invalid color string '" + s + "'"); +} + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const Color& c) +{ + out << (c.to_int() + 1); + return out; +} + +istream& operator>>(istream& in, Color& c) +{ + string s; + in >> s; + if (in) + { + s = to_lower(s); + if (s == "1" || s == "b" || s == "black") + { + c = Color(0); + return in; + } + else if (s == "2" || s == "w" || s == "white") + { + c = Color(1); + return in; + } + else if (s == "3") + { + c = Color(2); + return in; + } + else if (s == "4") + { + c = Color(3); + return in; + } + } + in.setstate(ios::failbit); + return in; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Color.h b/src/libpentobi_base/Color.h new file mode 100644 index 0000000..3d7a0da --- /dev/null +++ b/src/libpentobi_base/Color.h @@ -0,0 +1,190 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Color.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_COLOR_H +#define LIBPENTOBI_BASE_COLOR_H + +#include +#include +#include +#include +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +class Color +{ +public: + typedef uint_fast8_t IntType; + + class InvalidString + : public runtime_error + { + using runtime_error::runtime_error; + }; + + class Iterator + { + public: + explicit Iterator(IntType i) + { + m_i = i; + } + + bool operator==(Iterator it) const + { + return m_i == it.m_i; + } + + bool operator!=(Iterator it) const + { + return m_i != it.m_i; + } + + void operator++() + { + ++m_i; + } + + Color operator*() const + { + return Color(m_i); + } + + private: + IntType m_i; + }; + + class Range + { + public: + explicit Range(IntType nu_colors) + : m_nu_colors(nu_colors) + { } + + Iterator begin() const { return Iterator(0); } + + Iterator end() const { return Iterator(m_nu_colors); } + + private: + IntType m_nu_colors; + }; + + static const IntType range = 4; + + Color(); + + explicit Color(IntType i); + + explicit Color(const string& s); + + bool operator==(const Color& c) const; + + bool operator!=(const Color& c) const; + + bool operator<(const Color& c) const; + + IntType to_int() const; + + Color get_next(IntType nu_colors) const; + + Color get_previous(IntType nu_colors) const; + +private: + static const IntType value_uninitialized = range; + + IntType m_i; + + bool is_initialized() const; +}; + + +inline Color::Color() +{ +#if LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +inline Color::Color(IntType i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = i; +} + +inline bool Color::operator==(const Color& c) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(c.is_initialized()); + return m_i == c.m_i; +} + +inline bool Color::operator!=(const Color& c) const +{ + return ! operator==(c); +} + +inline bool Color::operator<(const Color& c) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(c.is_initialized()); + return m_i < c.m_i; +} + +inline Color Color::get_next(IntType nu_colors) const +{ + return Color(static_cast(m_i + 1) % nu_colors); +} + +inline Color Color::get_previous(IntType nu_colors) const +{ + return Color(static_cast(m_i + nu_colors - 1) % nu_colors); +} + +inline bool Color::is_initialized() const +{ + return m_i < value_uninitialized; +} + +inline Color::IntType Color::to_int() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +/** Output string representation of color. + The strings "1", "2", ... are used for the colors. */ +ostream& operator<<(ostream& out, const Color& c); + +/** Read color from input stream. + Accepts the strings "1", "2", ..., as well as "b", "w" or "black", "white" + for the first two colors. */ +istream& operator>>(istream& in, Color& c); + +//----------------------------------------------------------------------------- + +/** Unrolled loop over all colors. */ +template +inline void for_each_color(FUNCTION f) +{ + static_assert(Color::range == 4, ""); + f(Color(0)); + f(Color(1)); + f(Color(2)); + f(Color(3)); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_COLOR_H diff --git a/src/libpentobi_base/ColorMap.h b/src/libpentobi_base/ColorMap.h new file mode 100644 index 0000000..66b3610 --- /dev/null +++ b/src/libpentobi_base/ColorMap.h @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/ColorMap.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_COLOR_MAP_H +#define LIBPENTOBI_BASE_COLOR_MAP_H + +#include +#include "Color.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Container mapping a color to another element type. + The elements must be default-constructible. This requirement is due to the + fact that elements are stored in an array for efficient access by color + index and arrays need default-constructible elements. */ +template +class ColorMap +{ +public: + ColorMap() = default; + + explicit ColorMap(const T& val); + + T& operator[](Color c); + + const T& operator[](Color c) const; + + void fill(const T& val); + +private: + array m_a; +}; + +template +inline ColorMap::ColorMap(const T& val) +{ + fill(val); +} + +template +inline T& ColorMap::operator[](Color c) +{ + return m_a[c.to_int()]; +} + +template +inline const T& ColorMap::operator[](Color c) const +{ + return m_a[c.to_int()]; +} + +template +void ColorMap::fill(const T& val) +{ + m_a.fill(val); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_COLOR_MAP_H diff --git a/src/libpentobi_base/ColorMove.h b/src/libpentobi_base/ColorMove.h new file mode 100644 index 0000000..3bbe2e8 --- /dev/null +++ b/src/libpentobi_base/ColorMove.h @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/ColorMove.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_COLOR_MOVE_H +#define LIBPENTOBI_BASE_COLOR_MOVE_H + +#include "Move.h" +#include "libpentobi_base/Color.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +struct ColorMove +{ + Color color; + + Move move; + + /** Return a color move with a null move and an undefined color. + Even if the color is logically not defined, it is still initialized + (with Color(0)), such that this color move can be used in + comparisons. If you are sure that the color is never used and don't + want to initialize it for efficiency, use the default constructor + and then assign only the move. */ + static ColorMove null(); + + ColorMove() = default; + + ColorMove(Color c, Move mv); + + /** Equality operator. + @pre move, color, mv.move, mv.color are initialized. */ + bool operator==(const ColorMove& mv) const; + + /** Inequality operator. + @pre move, color, mv.move, mv.color are initialized. */ + bool operator!=(const ColorMove& mv) const; + + bool is_null() const; +}; + +inline ColorMove::ColorMove(Color c, Move mv) + : color(c), + move(mv) +{ +} + +inline bool ColorMove::operator==(const ColorMove& mv) const +{ + return move == mv.move && color == mv.color; +} + +inline bool ColorMove::operator!=(const ColorMove& mv) const +{ + return ! operator==(mv); +} + +inline bool ColorMove::is_null() const +{ + return move.is_null(); +} + +inline ColorMove ColorMove::null() +{ + return ColorMove(Color(0), Move::null()); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_COLOR_MOVE_H diff --git a/src/libpentobi_base/Engine.cpp b/src/libpentobi_base/Engine.cpp new file mode 100644 index 0000000..2b49931 --- /dev/null +++ b/src/libpentobi_base/Engine.cpp @@ -0,0 +1,379 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Engine.h" + +#include +#include "MoveMarker.h" +#include "PentobiTreeWriter.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/RandomGenerator.h" + +namespace libpentobi_base { + +using libboardgame_gtp::Failure; +using libboardgame_sgf::InvalidPropertyValue; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::get_last_node; +using libboardgame_util::ArrayList; +using libboardgame_util::RandomGenerator; + +//----------------------------------------------------------------------------- + +Engine::Engine(Variant variant) + : m_game(variant) +{ + add("all_legal", &Engine::cmd_all_legal); + add("clear_board", &Engine::cmd_clear_board); + add("final_score", &Engine::cmd_final_score); + add("get_place", &Engine::cmd_get_place); + add("loadsgf", &Engine::cmd_loadsgf); + add("point_integers", &Engine::cmd_point_integers); + add("move_info", &Engine::cmd_move_info); + add("p", &Engine::cmd_p); + add("param_base", &Engine::cmd_param_base); + add("play", &Engine::cmd_play); + add("savesgf", &Engine::cmd_savesgf); + add("set_game", &Engine::cmd_set_game); + add("showboard", &Engine::cmd_showboard); + add("undo", &Engine::cmd_undo); +} + +void Engine::board_changed() +{ + if (m_show_board) + LIBBOARDGAME_LOG(get_board()); +} + +void Engine::cmd_all_legal(const Arguments& args, Response& response) +{ + auto& bd = get_board(); + unique_ptr moves(new MoveList); + unique_ptr marker(new MoveMarker); + bd.gen_moves(get_color_arg(args), *marker, *moves); + for (Move mv : *moves) + response << bd.to_string(mv, false) << '\n'; +} + +void Engine::cmd_clear_board() +{ + m_game.init(); + board_changed(); +} + +void Engine::cmd_final_score(Response& response) +{ + auto& bd = get_board(); + if (get_nu_players(bd.get_variant()) > 2) + { + for (Color c : bd.get_colors()) + response << bd.get_points(c) << ' '; + } + else + { + auto score = bd.get_score_twoplayer(Color(0)); + if (score > 0) + response << "B+" << score; + else if (score < 0) + response << "W+" << (-score); + else + response << "0"; + } +} + +void Engine::cmd_g(Response& response) +{ + genmove(get_board().get_effective_to_play(), response); +} + +void Engine::cmd_genmove(const Arguments& args, Response& response) +{ + genmove(get_color_arg(args), response); +} + +void Engine::cmd_get_place(const Arguments& args, Response& response) +{ + auto& bd = get_board(); + unsigned place; + bool isPlaceShared; + bd.get_place(get_color_arg(args), place, isPlaceShared); + response << place; + if (isPlaceShared) + response << " shared"; +} + +void Engine::cmd_loadsgf(const Arguments& args) +{ + args.check_size_less_equal(2); + string file = args.get(0); + int move_number = -1; + if (args.get_size() == 2) + move_number = args.parse_min(1, 1) - 1; + try + { + TreeReader reader; + reader.read(file); + auto tree = reader.get_tree_transfer_ownership(); + m_game.init(tree); + const SgfNode* node = nullptr; + if (move_number != -1) + node = m_game.get_tree().get_node_before_move_number(move_number); + if (! node) + node = &get_last_node(m_game.get_root()); + m_game.goto_node(*node); + board_changed(); + } + catch (const runtime_error& e) + { + throw Failure(e.what()); + } +} + +/** Return move info of a move given by its integer or string representation. */ +void Engine::cmd_move_info(const Arguments& args, Response& response) +{ + auto& bd = get_board(); + Move mv; + try + { + mv = Move(args.parse()); + } + catch (const Failure&) + { + try + { + mv = bd.from_string(args.get()); + } + catch (const runtime_error&) + { + ostringstream msg; + msg << "invalid argument '" << args.get() + << "' (expected move or move ID)"; + throw Failure(msg.str()); + } + } + auto& geo = bd.get_geometry(); + Piece piece = bd.get_move_piece(mv); + auto& info_ext_2 = bd.get_move_info_ext_2(mv); + response + << "\n" + << "ID: " << mv.to_int() << "\n" + << "Piece: " << static_cast(piece.to_int()) + << " (" << bd.get_piece_info(piece).get_name() << ")\n" + << "Points:"; + for (Point p : bd.get_move_points(mv)) + response << ' ' << geo.to_string(p); + response + << "\n" + << "BrkSym: " << info_ext_2.breaks_symmetry << "\n" + << "SymMv: " << bd.to_string(info_ext_2.symmetric_move); +} + +void Engine::cmd_p(const Arguments& args) +{ + play(get_board().get_to_play(), args, 0); +} + +void Engine::cmd_param_base(const Arguments& args, Response& response) +{ + if (args.get_size() == 0) + response + << "accept_illegal " << m_accept_illegal << '\n' + << "resign " << m_resign << '\n'; + else + { + args.check_size(2); + string name = args.get(0); + if (name == "accept_illegal") + m_accept_illegal = args.parse(1); + else if (name == "resign") + m_resign = args.parse(1); + else + { + ostringstream msg; + msg << "unknown parameter '" << name << "'"; + throw Failure(msg.str()); + } + } +} + +void Engine::cmd_play(const Arguments& args) +{ + play(get_color_arg(args, 0), args, 1); +} + +void Engine::cmd_point_integers(Response& response) +{ + auto& geo = get_board().get_geometry(); + Grid grid; + for (Point p : geo) + grid[p] = p.to_int(); + response << '\n' << grid.to_string(geo); +} + +void Engine::cmd_reg_genmove(const Arguments& args, Response& response) +{ + RandomGenerator::set_global_seed_last(); + Move move = get_player().genmove(get_board(), get_color_arg(args)); + if (move.is_null()) + throw Failure("player failed to generate a move"); + response << get_board().to_string(move, false); +} + +void Engine::cmd_savesgf(const Arguments& args) +{ + ofstream out(args.get()); + PentobiTreeWriter writer(out, m_game.get_tree()); + writer.set_indent(1); + writer.write(); + if (! out) + throw Failure(strerror(errno)); +} + +/** Set the game variant. + Argument: game variant as in GM property of Pentobi SGF files +
+ This command is similar to the command that is used by Quarry + (http://home.gna.org/quarry/) to set a game at GTP engines that support + multiple games. */ +void Engine::cmd_set_game(const Arguments& args) +{ + Variant variant; + if (! parse_variant(args.get_line(), variant)) + throw Failure("invalid argument"); + m_game.init(variant); + board_changed(); +} + +void Engine::cmd_showboard(Response& response) +{ + response << '\n' << get_board(); +} + +void Engine::cmd_undo() +{ + auto& bd = get_board(); + if (bd.get_nu_moves() == 0) + throw Failure("cannot undo"); + m_game.undo(); + board_changed(); +} + +void Engine::genmove(Color c, Response& response) +{ + auto& bd = get_board(); + auto& player = get_player(); + auto mv = player.genmove(bd, c); + if (mv.is_null()) + { + response << "pass"; + return; + } + if (! bd.is_legal(c, mv)) + { + ostringstream msg; + msg << "player generated illegal move: " << bd.to_string(mv); + throw Failure(msg.str()); + } + if (m_resign && player.resign()) + { + response << "resign"; + return; + } + m_game.play(c, mv, true); + response << bd.to_string(mv, false); + board_changed(); +} + +Color Engine::get_color_arg(const Arguments& args) const +{ + if (args.get_size() > 1) + throw Failure("too many arguments"); + return get_color_arg(args, 0); +} + +Color Engine::get_color_arg(const Arguments& args, unsigned i) const +{ + string s = args.get_tolower(i); + auto& bd = get_board(); + auto variant = bd.get_variant(); + if (get_nu_colors(variant) == 2) + { + if (s == "blue" || s == "black" || s == "b") + return Color(0); + if (s == "green" || s == "white" || s == "w") + return Color(1); + } + else + { + if (s == "1" || s == "blue") + return Color(0); + if (s == "2" || s == "yellow") + return Color(1); + if (s == "3" || s == "red") + return Color(2); + if (s == "4" || s == "green") + return Color(3); + } + throw Failure("invalid color argument '" + s + "'"); +} + +PlayerBase& Engine::get_player() const +{ + if (! m_player) + throw Failure("no player set"); + return *m_player; +} + +void Engine::play(Color c, const Arguments& args, unsigned arg_move_begin) +{ + auto& bd = get_board(); + if (bd.get_nu_moves() >= Board::max_game_moves) + throw Failure("too many moves"); + Move mv; + try + { + if (arg_move_begin == 0) + mv = bd.from_string(args.get_line()); + else + mv = bd.from_string(args.get_remaining_line(arg_move_begin - 1)); + } + catch (const runtime_error& e) + { + throw Failure(e.what()); + } + if (mv.is_null()) + throw Failure("play pass not supported (anymore)"); + if (! m_accept_illegal && ! bd.is_legal(c, mv)) + throw Failure("illegal move"); + m_game.play(c, mv, true); + board_changed(); +} + +void Engine::set_player(PlayerBase& player) +{ + m_player = &player; + add("genmove", &Engine::cmd_genmove); + add("g", &Engine::cmd_g); + add("reg_genmove", &Engine::cmd_reg_genmove); +} + +void Engine::set_show_board(bool enable) +{ + if (enable && ! m_show_board) + LIBBOARDGAME_LOG(get_board()); + m_show_board = enable; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Engine.h b/src/libpentobi_base/Engine.h new file mode 100644 index 0000000..d4e1493 --- /dev/null +++ b/src/libpentobi_base/Engine.h @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Engine.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_ENGINE_H +#define LIBPENTOBI_BASE_ENGINE_H + +#include "libpentobi_base/Game.h" +#include "libpentobi_base/PlayerBase.h" +#include "libboardgame_base/Engine.h" + +namespace libpentobi_base { + +using namespace std; +using libboardgame_gtp::Arguments; +using libboardgame_gtp::Response; + +//----------------------------------------------------------------------------- + +/** GTP Blokus engine. */ +class Engine + : public libboardgame_base::Engine +{ +public: + explicit Engine(Variant variant); + + void cmd_all_legal(const Arguments&, Response&); + void cmd_clear_board(); + void cmd_final_score(Response&); + void cmd_g(Response&); + void cmd_genmove(const Arguments&, Response&); + void cmd_get_place(const Arguments& args, Response&); + void cmd_loadsgf(const Arguments&); + void cmd_move_info(const Arguments&, Response&); + void cmd_p(const Arguments&); + void cmd_param_base(const Arguments&, Response&); + void cmd_play(const Arguments&); + void cmd_point_integers(Response&); + void cmd_showboard(Response&); + void cmd_reg_genmove(const Arguments&, Response&); + void cmd_savesgf(const Arguments&); + void cmd_set_game(const Arguments&); + void cmd_undo(); + + /** Set the player. + @param player The player (@ref libboardgame_doc_storesref) */ + void set_player(PlayerBase& player); + + void set_accept_illegal(bool enable); + + /** Enable or disable resigning. */ + void set_resign(bool enable); + + void set_show_board(bool enable); + + const Board& get_board() const; + +protected: + Color get_color_arg(const Arguments& args, unsigned i) const; + + Color get_color_arg(const Arguments& args) const; + +private: + bool m_accept_illegal = false; + + bool m_show_board = false; + + bool m_resign = true; + + Game m_game; + + PlayerBase* m_player = nullptr; + + void board_changed(); + + void genmove(Color c, Response& response); + + PlayerBase& get_player() const; + + void play(Color c, const Arguments& args, unsigned arg_move_begin); +}; + +inline const Board& Engine::get_board() const +{ + return m_game.get_board(); +} + +inline void Engine::set_accept_illegal(bool enable) +{ + m_accept_illegal = enable; +} + +inline void Engine::set_resign(bool enable) +{ + m_resign = enable; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_ENGINE_H diff --git a/src/libpentobi_base/Game.cpp b/src/libpentobi_base/Game.cpp new file mode 100644 index 0000000..ce39236 --- /dev/null +++ b/src/libpentobi_base/Game.cpp @@ -0,0 +1,191 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Game.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Game.h" + +#include "BoardUtil.h" +#include "libboardgame_sgf/InvalidTree.h" +#include "libboardgame_sgf/SgfUtil.h" + +namespace libpentobi_base { + +using libboardgame_sgf::InvalidTree; +using libboardgame_sgf::util::back_to_main_variation; +using libboardgame_sgf::util::is_main_variation; +using libpentobi_base::boardutil::get_current_position_as_setup; + +//----------------------------------------------------------------------------- + +Game::Game(Variant variant) + : m_bd(new Board(variant)), + m_tree(variant) +{ + init(variant); +} + +Game::~Game() = default; + +void Game::add_setup(Color c, Move mv) +{ + auto& node = m_tree.add_setup(*m_current, c, mv); + goto_node(node); +} + +void Game::delete_all_variations() +{ + goto_node(back_to_main_variation(*m_current)); + m_tree.delete_all_variations(); +} + +Color Game::get_to_play_default(const Game& game) +{ + auto& tree = game.get_tree(); + auto& bd = game.get_board(); + auto node = &game.get_current(); + Color next = Color(0); + while (node) + { + auto mv = tree.get_move(*node); + if (! mv.is_null()) + { + next = bd.get_next(mv.color); + break; + } + Color c; + if (libpentobi_base::node_util::get_player(*node, c)) + return c; + node = node->get_parent_or_null(); + } + return bd.get_effective_to_play(next); +} + +void Game::goto_node(const SgfNode& node) +{ + auto old = m_current; + try + { + update(node); + } + catch (const InvalidTree&) + { + // Try to restore the old state. + if (! old) + m_current = &node; + else + { + try + { + update(*old); + } + catch (const InvalidTree&) + { + } + } + throw; + } +} + +void Game::init(Variant variant) +{ + m_bd->init(variant); + m_tree.init_variant(variant); + m_current = &m_tree.get_root(); +} + +void Game::init(unique_ptr& root) +{ + m_tree.init(root); + m_bd->init(m_tree.get_variant()); + m_current = nullptr; + goto_node(m_tree.get_root()); +} + +void Game::keep_only_position() +{ + m_tree.keep_only_subtree(*m_current); + m_tree.remove_children(m_tree.get_root()); + m_current = nullptr; + goto_node(m_tree.get_root()); +} + +void Game::keep_only_subtree() +{ + m_tree.keep_only_subtree(*m_current); + m_current = nullptr; + goto_node(m_tree.get_root()); +} + +void Game::play(ColorMove mv, bool always_create_new_node) +{ + m_bd->play(mv); + const SgfNode* child = nullptr; + if (! always_create_new_node) + child = m_tree.find_child_with_move(*m_current, mv); + if (child) + m_current = child; + else + { + m_current = &m_tree.create_new_child(*m_current); + m_tree.set_move(*m_current, mv); + } + set_to_play(get_to_play_default(*this)); +} + +void Game::remove_player() +{ + m_tree.remove_player(*m_current); + update(*m_current); +} + +void Game::remove_setup(Color c, Move mv) +{ + auto& node = m_tree.remove_setup(*m_current, c, mv); + goto_node(node); +} + +void Game::set_player(Color c) +{ + m_tree.set_player(*m_current, c); + update(*m_current); +} + +void Game::set_result(int score) +{ + if (is_main_variation(*m_current)) + m_tree.set_result(m_tree.get_root(), score); +} + +void Game::set_to_play(Color c) +{ + m_bd->set_to_play(c); +} + +void Game::truncate() +{ + goto_node(m_tree.truncate(*m_current)); +} + +void Game::undo() +{ + LIBBOARDGAME_ASSERT(! m_tree.get_move(*m_current).is_null()); + LIBBOARDGAME_ASSERT(m_current->has_parent()); + truncate(); +} + +void Game::update(const SgfNode& node) +{ + m_updater.update(*m_bd, m_tree, node); + m_current = &node; + set_to_play(get_to_play_default(*this)); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Game.h b/src/libpentobi_base/Game.h new file mode 100644 index 0000000..0f5fa5c --- /dev/null +++ b/src/libpentobi_base/Game.h @@ -0,0 +1,429 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Game.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_GAME_H +#define LIBPENTOBI_BASE_GAME_H + +#include "Board.h" +#include "BoardUpdater.h" +#include "NodeUtil.h" +#include "PentobiTree.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +class Game +{ +public: + /** Determine a sensible value for the color to play at the current node. + If the color was explicitely set with a setup property, it will be + used. Otherwise, the effective color to play will be used, starting + with the next color of the color of the last move (see + Board::get_effective_to_play(Color)) */ + static Color get_to_play_default(const Game& game); + + + explicit Game(Variant variant); + + ~Game(); + + + void init(Variant variant); + + void init(); + + /** Initialize game from a SGF tree. + @note If the tree contains invalid properties, future calls to + goto_node() might throw an exception. + @param root The root node of the SGF tree; the ownership is transferred + to this class. + @throws InvalidTree, if the root node contains invalid + properties */ + void init(unique_ptr& root); + + const Board& get_board() const; + + Variant get_variant() const; + + const SgfNode& get_current() const; + + const SgfNode& get_root() const; + + const PentobiTree& get_tree() const; + + /** Get the current color to play. + Initialized with get_to_play_default() but may be changed with + set_to_play(). */ + Color get_to_play() const; + + /** @param mv + @param always_create_new_node Always create a new child of the current + node even if a child with the move already exists. */ + void play(ColorMove mv, bool always_create_new_node); + + void play(Color c, Move mv, bool always_create_new_node); + + /** Update game state to a node in the tree. + @throws InvalidTree, if the game was constructed with an + external SGF tree and the tree contained invalid property values + (syntactically or sematically, like moves on occupied points). If an + exception is thrown, the current node is not changed. */ + void goto_node(const SgfNode& node); + + /** Undo the current move and go to parent node. + @pre ! get_current().get_move().is_null() + @pre get_current()->has_parent() + @note Even if the implementation of this function calls goto_node(), + it cannot throw an InvalidPropertyValue because the class Game ensures + that the current node is always reachable via a path of nodes with + valid move properties. */ + void undo(); + + /** Set the current color to play. + Does not store a player property in the tree or affect what color is to + play when navigating away from and back to the current node. */ + void set_to_play(Color c); + + ColorMove get_move() const; + + /** See libpentobi_base::Tree::get_move_ignore_invalid() */ + ColorMove get_move_ignore_invalid() const; + + /** Add final score to root node if the current node is in the main + variation. */ + void set_result(int score); + + void set_charset(const string& charset); + + void remove_move_annotation(); + + double get_bad_move() const; + + double get_good_move() const; + + bool is_doubtful_move() const; + + bool is_interesting_move() const; + + void set_bad_move(double value = 1); + + void set_good_move(double value = 1); + + void set_doubtful_move(); + + void set_interesting_move(); + + string get_comment() const; + + void set_comment(const string& s); + + /** Delete the current node and its subtree and go to the parent node. + @pre get_current().has_parent() */ + void truncate(); + + void truncate_children(); + + /** Replace the game tree by a new one that has the current position + as a setup in its root node. */ + void keep_only_position(); + + /** Like keep_only_position() but does not delete the children of the + current node. */ + void keep_only_subtree(); + + void make_main_variation(); + + void move_up_variation(); + + void move_down_variation(); + + /** Delete all variations but the main variation. + If the current node is not in the main variation it will be changed + to the node as in libboardgame_sgf::util::back_to_main_variation() */ + void delete_all_variations(); + + /** Make the current node the first child of its parent. */ + void make_first_child(); + + void set_modified(); + + void clear_modified(); + + bool is_modified() const; + + /** Set the AP property at the root node. */ + void set_application(const string& name, const string& version = ""); + + string get_player_name(Color c) const; + + void set_player_name(Color c, const string& name); + + string get_date() const; + + void set_date(const string& date); + + void set_date_today(); + + /** Get event info (standard property EV) from root node. */ + string get_event() const; + + void set_event(const string& event); + + /** Get round info (standard property RO) from root node. */ + string get_round() const; + + void set_round(const string& round); + + /** Get time info (standard property TM) from root node. */ + string get_time() const; + + void set_time(const string& time); + + bool has_setup() const; + + void add_setup(Color c, Move mv); + + void remove_setup(Color c, Move mv); + + /** See libpentobi_base::Tree::set_player() */ + void set_player(Color c); + + /** See libpentobi_base::Tree::remove_player() */ + void remove_player(); + +private: + const SgfNode* m_current; + + unique_ptr m_bd; + + PentobiTree m_tree; + + BoardUpdater m_updater; + + void update(const SgfNode& node); +}; + +inline void Game::clear_modified() +{ + m_tree.clear_modified(); +} + +inline double Game::get_bad_move() const +{ + return m_tree.get_bad_move(*m_current); +} + +inline const Board& Game::get_board() const +{ + return *m_bd; +} + +inline string Game::get_comment() const +{ + return m_tree.get_comment(*m_current); +} + +inline string Game::get_date() const +{ + return m_tree.get_date(); +} + +inline string Game::get_event() const +{ + return m_tree.get_event(); +} + +inline const SgfNode& Game::get_current() const +{ + return *m_current; +} + +inline double Game::get_good_move() const +{ + return m_tree.get_good_move(*m_current); +} + +inline ColorMove Game::get_move() const +{ + return m_tree.get_move(*m_current); +} + +inline ColorMove Game::get_move_ignore_invalid() const +{ + return m_tree.get_move_ignore_invalid(*m_current); +} + +inline string Game::get_player_name(Color c) const +{ + return m_tree.get_player_name(c); +} + +inline Color Game::get_to_play() const +{ + return m_bd->get_to_play(); +} + +inline string Game::get_round() const +{ + return m_tree.get_round(); +} + +inline const SgfNode& Game::get_root() const +{ + return m_tree.get_root(); +} + +inline string Game::get_time() const +{ + return m_tree.get_time(); +} + +inline const PentobiTree& Game::get_tree() const +{ + return m_tree; +} + +inline bool Game::has_setup() const +{ + return libpentobi_base::node_util::has_setup(*m_current); +} + +inline Variant Game::get_variant() const +{ + return m_bd->get_variant(); +} + +inline void Game::init() +{ + init(m_bd->get_variant()); +} + +inline bool Game::is_doubtful_move() const +{ + return m_tree.is_doubtful_move(*m_current); +} + +inline bool Game::is_interesting_move() const +{ + return m_tree.is_interesting_move(*m_current); +} + +inline bool Game::is_modified() const +{ + return m_tree.is_modified(); +} + +inline void Game::make_first_child() +{ + m_tree.make_first_child(*m_current); +} + +inline void Game::make_main_variation() +{ + m_tree.make_main_variation(*m_current); +} + +inline void Game::move_down_variation() +{ + m_tree.move_down(*m_current); +} + +inline void Game::move_up_variation() +{ + m_tree.move_up(*m_current); +} + +inline void Game::play(Color c, Move mv, bool always_create_new_node) +{ + play(ColorMove(c, mv), always_create_new_node); +} + +inline void Game::remove_move_annotation() +{ + m_tree.remove_move_annotation(*m_current); +} + +inline void Game::set_application(const string& name, const string& version) +{ + m_tree.set_application(name, version); +} + +inline void Game::set_bad_move(double value) +{ + m_tree.set_bad_move(*m_current, value); +} + +inline void Game::set_charset(const string& charset) +{ + m_tree.set_charset(charset); +} + +inline void Game::set_comment(const string& s) +{ + m_tree.set_comment(*m_current, s); +} + +inline void Game::set_date(const string& date) +{ + m_tree.set_date(date); +} + +inline void Game::set_event(const string& event) +{ + m_tree.set_event(event); +} + +inline void Game::set_date_today() +{ + m_tree.set_date_today(); +} + +inline void Game::set_doubtful_move() +{ + m_tree.set_doubtful_move(*m_current); +} + +inline void Game::set_good_move(double value) +{ + m_tree.set_good_move(*m_current, value); +} + +inline void Game::set_interesting_move() +{ + m_tree.set_interesting_move(*m_current); +} + +inline void Game::set_modified() +{ + m_tree.set_modified(); +} + +inline void Game::set_player_name(Color c, const string& name) +{ + m_tree.set_player_name(c, name); +} + +inline void Game::set_round(const string& round) +{ + m_tree.set_round(round); +} + +inline void Game::set_time(const string& time) +{ + m_tree.set_time(time); +} + +inline void Game::truncate_children() +{ + m_tree.remove_children(*m_current); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_GAME_H diff --git a/src/libpentobi_base/Geometry.h b/src/libpentobi_base/Geometry.h new file mode 100644 index 0000000..cff9b28 --- /dev/null +++ b/src/libpentobi_base/Geometry.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Geometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_GEOMETRY_H +#define LIBPENTOBI_BASE_GEOMETRY_H + +#include "Point.h" +#include "libboardgame_base/Geometry.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +typedef libboardgame_base::Geometry Geometry; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_GEOMETRY_H diff --git a/src/libpentobi_base/Grid.h b/src/libpentobi_base/Grid.h new file mode 100644 index 0000000..78d4e72 --- /dev/null +++ b/src/libpentobi_base/Grid.h @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Grid.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_GRID_H +#define LIBPENTOBI_BASE_GRID_H + +#include "Point.h" +#include "libboardgame_base/Grid.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +template +using Grid = libboardgame_base::Grid; + +template +using GridExt = libboardgame_base::GridExt; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_GRID_H diff --git a/src/libpentobi_base/Marker.h b/src/libpentobi_base/Marker.h new file mode 100644 index 0000000..eb867c9 --- /dev/null +++ b/src/libpentobi_base/Marker.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Marker.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MARKER_H +#define LIBPENTOBI_BASE_MARKER_H + +#include "Point.h" +#include "libboardgame_base/Marker.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +typedef libboardgame_base::Marker Marker; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_MARKER_H diff --git a/src/libpentobi_base/Move.h b/src/libpentobi_base/Move.h new file mode 100644 index 0000000..1022ca6 --- /dev/null +++ b/src/libpentobi_base/Move.h @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Move.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MOVE_H +#define LIBPENTOBI_BASE_MOVE_H + +#include +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +class Move +{ +public: + /** Integer type used internally in this class to store a move. + This class is optimized for size not for speed because there are + large precomputed data structures that store moves and move lists. + Therefore it uses uint_least16_t, not uint_fast16_t. */ + typedef uint_least16_t IntType; + + static const IntType onboard_moves_classic = 30433; + + static const IntType onboard_moves_trigon = 32131; + + static const IntType onboard_moves_trigon_3 = 24859; + + static const IntType onboard_moves_duo = 13729; + + static const IntType onboard_moves_junior = 7217; + + static const IntType onboard_moves_nexos = 15157; + + static const IntType onboard_moves_callisto = 9433; + + static const IntType onboard_moves_callisto_2 = 4265; + + static const IntType onboard_moves_callisto_3 = 6885; + + /** Integer range of moves. + The maximum is given by the number of on-board moves in game variant + Trigon, plus a null move. */ + static const IntType range = onboard_moves_trigon + 1; + + static Move null(); + + Move(); + + explicit Move(IntType i); + + bool operator==(const Move& mv) const; + + bool operator!=(const Move& mv) const; + + bool operator<(const Move& mv) const; + + bool is_null() const; + + /** Return move as an integer between 0 and Move::range */ + IntType to_int() const; + +private: + static const IntType value_uninitialized = range; + + IntType m_i; + + bool is_initialized() const; +}; + +inline Move::Move() +{ +#if LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +inline Move::Move(IntType i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = i; +} + +inline bool Move::operator==(const Move& mv) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(mv.is_initialized()); + return m_i == mv.m_i; +} + +inline bool Move::operator!=(const Move& mv) const +{ + return ! operator==(mv); +} + +inline bool Move::operator<(const Move& mv) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(mv.is_initialized()); + return m_i < mv.m_i; +} + +inline bool Move::is_initialized() const +{ + return m_i < value_uninitialized; +} + +inline bool Move::is_null() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i == 0; +} + +inline Move Move::null() +{ + return Move(0); +} + +inline Move::IntType Move::to_int() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_MOVE_H diff --git a/src/libpentobi_base/MoveInfo.h b/src/libpentobi_base/MoveInfo.h new file mode 100644 index 0000000..3cd8921 --- /dev/null +++ b/src/libpentobi_base/MoveInfo.h @@ -0,0 +1,135 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/MoveInfo.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MOVE_INFO_H +#define LIBPENTOBI_BASE_MOVE_INFO_H + +#include "Move.h" +#include "MovePoints.h" +#include "Piece.h" +#include "PieceInfo.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Most frequently accessed move info. + Contains the points and the piece of the move. If the point list is smaller + than MAX_SIZE, values above end() up to MAX_SIZE may be accessed and + contain Point::null() to allow loop unrolling. The points correspond to + PieceInfo::get_points(), which includes certain junction points in Nexos, + see comment there. + Since this is the most performance-critical data structure, it takes + a template argument to make the space for move points not larger than + needed in the current game variant. */ +template +class MoveInfo +{ +public: + MoveInfo() = default; + + MoveInfo(Piece piece, const MovePoints& points) + { + m_piece = static_cast(piece.to_int()); + m_size = static_cast(points.size()); + for (MovePoints::IntType i = 0; i < MAX_SIZE; ++i) + m_points[i] = points.get_unchecked(i); + } + + const Point* begin() const { return m_points; } + + const Point* end() const { return m_points + m_size; } + + Piece get_piece() const { return Piece(m_piece); } + + unsigned get_size() const { return m_size; } + +private: + uint_least8_t m_piece; + + uint_least8_t m_size; + + Point m_points[MAX_SIZE]; +}; + +//----------------------------------------------------------------------------- + +/** Less frequently accessed move info. + Stored separately from move points and move piece to improve CPU cache + performance. + Since this is a performance-critical data structure, it takes + a template argument to make the space for move points not larger than + needed in the current game variant. + @tparam MAX_ADJ_ATTACH Maximum total number of attach points and adjacent + points of a piece in the corresponding game variant. */ +template +struct MoveInfoExt +{ + /** Concatenated list of adjacent and attach points. */ + Point points[MAX_ADJ_ATTACH]; + + uint_least8_t size_attach_points; + + uint_least8_t size_adj_points; + + const Point* begin_adj() const { return points; } + + const Point* end_adj() const { return points + size_adj_points; } + + const Point* begin_attach() const { return end_adj(); } + + const Point* end_attach() const + { + return begin_attach() + size_attach_points; + } +}; + +//----------------------------------------------------------------------------- + +/** Least frequently accessed move info. + Stored separately from move points and move piece to improve CPU cache + performance. */ +struct MoveInfoExt2 +{ + /** Whether the move breaks rotational symmetry of the board. + Currently not initialized for classic and trigon_3 board types because + enforced rotational-symmetric draws are not used in the MCTS search on + these boards (trigon_3 has no 2-player game variant and classic_2 + currently only supports colored starting points, which makes rotational + draws impossible. */ + bool breaks_symmetry; + + uint_least8_t scored_points_size; + + /** The rotational-symmetric counterpart to this move. + Only initalized for game variants that have rotational-symmetric boards + and starting points. */ + Move symmetric_move; + + Point label_pos; + + /** The points of a move that contribute to the score, which excludes + junction points in Nexos. */ + Point scored_points[PieceInfo::max_scored_size]; + + + const Point* begin_scored_points() const { return scored_points; } + + const Point* end_scored_points() const + { + return scored_points + scored_points_size; + } +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_MOVE_INFO_H diff --git a/src/libpentobi_base/MoveList.h b/src/libpentobi_base/MoveList.h new file mode 100644 index 0000000..9f278e9 --- /dev/null +++ b/src/libpentobi_base/MoveList.h @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/MoveList.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MOVE_LIST_H +#define LIBPENTOBI_BASE_MOVE_LIST_H + +#include "Move.h" +#include "libboardgame_util/ArrayList.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** List that can hold all possible moves, not including Move::null() */ +typedef libboardgame_util::ArrayList MoveList; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_MOVE_LIST_H diff --git a/src/libpentobi_base/MoveMarker.h b/src/libpentobi_base/MoveMarker.h new file mode 100644 index 0000000..5e205af --- /dev/null +++ b/src/libpentobi_base/MoveMarker.h @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/MoveMarker.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MOVE_MARKER_H +#define LIBPENTOBI_BASE_MOVE_MARKER_H + +#include +#include "Move.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +class MoveMarker +{ +public: + MoveMarker() + { + clear(); + } + + bool operator[](Move mv) const + { + return m_a[mv.to_int()]; + } + + void set(Move mv) + { + m_a[mv.to_int()] = true; + } + + void clear(Move mv) + { + m_a[mv.to_int()] = false; + } + + template + void set(const T& t) + { + for (Move mv : t) + set(mv); + } + + template + void clear(const T& t) + { + for (Move mv : t) + clear(mv); + } + + void set() + { + m_a.fill(true); + } + + void clear() + { + m_a.fill(false); + } + +private: + array m_a; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_MOVE_MARKER_H diff --git a/src/libpentobi_base/MovePoints.h b/src/libpentobi_base/MovePoints.h new file mode 100644 index 0000000..1383234 --- /dev/null +++ b/src/libpentobi_base/MovePoints.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/MovePoints.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MOVE_POINTS_H +#define LIBPENTOBI_BASE_MOVE_POINTS_H + +#include "PieceInfo.h" +#include "Point.h" +#include "libboardgame_util/ArrayList.h" + +namespace libpentobi_base { + +using libboardgame_util::ArrayList; + +//----------------------------------------------------------------------------- + +typedef ArrayList MovePoints; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_MOVE_POINTS_H diff --git a/src/libpentobi_base/NexosGeometry.cpp b/src/libpentobi_base/NexosGeometry.cpp new file mode 100644 index 0000000..29df080 --- /dev/null +++ b/src/libpentobi_base/NexosGeometry.cpp @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/NexosGeometry.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "NexosGeometry.h" + +#include "libboardgame_util/Unused.h" + +namespace libpentobi_base { + +using libboardgame_base::CoordPoint; + +//----------------------------------------------------------------------------- + +map> NexosGeometry::s_geometry; + +NexosGeometry::NexosGeometry(unsigned sz) +{ + Geometry::init(sz * 2 - 1, sz * 2 - 1); +} + +const NexosGeometry& NexosGeometry::get(unsigned sz) +{ + auto pos = s_geometry.find(sz); + if (pos != s_geometry.end()) + return *pos->second; + shared_ptr geometry(new NexosGeometry(sz)); + return *s_geometry.insert(make_pair(sz, geometry)).first->second; +} + +auto NexosGeometry::get_adj_coord(int x, int y) const -> AdjCoordList +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return AdjCoordList(); +} + +auto NexosGeometry::get_diag_coord(int x, int y) const -> DiagCoordList +{ + DiagCoordList l; + if (get_point_type(x, y) == 1) + { + l.push_back(CoordPoint(x - 2, y)); + l.push_back(CoordPoint(x + 2, y)); + l.push_back(CoordPoint(x - 1, y - 1)); + l.push_back(CoordPoint(x + 1, y + 1)); + l.push_back(CoordPoint(x - 1, y + 1)); + l.push_back(CoordPoint(x + 1, y - 1)); + } + else if (get_point_type(x, y) == 2) + { + l.push_back(CoordPoint(x, y - 2)); + l.push_back(CoordPoint(x, y + 2)); + l.push_back(CoordPoint(x - 1, y - 1)); + l.push_back(CoordPoint(x + 1, y + 1)); + l.push_back(CoordPoint(x - 1, y + 1)); + l.push_back(CoordPoint(x + 1, y - 1)); + } + return l; +} + +unsigned NexosGeometry::get_period_x() const +{ + return 2; +} + +unsigned NexosGeometry::get_period_y() const +{ + return 2; +} + +unsigned NexosGeometry::get_point_type(int x, int y) const +{ + if (x % 2 == 0) + return y % 2 == 0 ? 0 : 2; + else + return y % 2 == 0 ? 1 : 3; +} + +bool NexosGeometry::init_is_onboard(unsigned x, unsigned y) const +{ + return x < get_width() && y < get_height() && get_point_type(x, y) != 3; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + diff --git a/src/libpentobi_base/NexosGeometry.h b/src/libpentobi_base/NexosGeometry.h new file mode 100644 index 0000000..58c7909 --- /dev/null +++ b/src/libpentobi_base/NexosGeometry.h @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/NexosGeometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_NEXOS_GEOMETRY_H +#define LIBPENTOBI_BASE_NEXOS_GEOMETRY_H + +#include +#include +#include "Geometry.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Geometry as used in the game Nexos. + The points of the board are horizontal or vertical segments and junctions. + Junctions only need to be included in piece definitions if they are + necessary to indicate that the opponent cannot cross the junction + (i.e. if exactly two segments of the piece with the same orientation + connect to the junction). + The coordinates are like: + + 0 1 2 3 4 5 6 ... + 0 + - + - + - + + 1 | | | | + 2 + - + - + - + + 3 | | | | + 4 + - + - + - + + + There are four point types: 0=junction, 1=horizontal segment, 2=vertical + segment, 3=hole surrounded by segments. + To fit with the generalizations used in the Blokus engine, points have no + adjacent points, and points are diagonal to each other if they are segments + that connect to the same junction. */ +class NexosGeometry final + : public Geometry +{ +public: + /** Create or reuse an already created geometry with a given size. + @param sz The number of segments in a row or column. */ + static const NexosGeometry& get(unsigned sz); + + + AdjCoordList get_adj_coord(int x, int y) const override; + + DiagCoordList get_diag_coord(int x, int y) const override; + + unsigned get_point_type(int x, int y) const override; + + unsigned get_period_x() const override; + + unsigned get_period_y() const override; + +protected: + bool init_is_onboard(unsigned x, unsigned y) const override; + +private: + /** Stores already created geometries by size. */ + static map> s_geometry; + + + explicit NexosGeometry(unsigned sz); +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_NEXOS_GEOMETRY_H diff --git a/src/libpentobi_base/NodeUtil.cpp b/src/libpentobi_base/NodeUtil.cpp new file mode 100644 index 0000000..1b7b513 --- /dev/null +++ b/src/libpentobi_base/NodeUtil.cpp @@ -0,0 +1,175 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/NodeUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "NodeUtil.h" + +#include "libboardgame_util/StringUtil.h" + +namespace libpentobi_base { +namespace node_util { + +using libboardgame_sgf::InvalidPropertyValue; +using libboardgame_sgf::InvalidTree; +using libboardgame_util::split; +using libboardgame_util::trim; + +//----------------------------------------------------------------------------- + +bool get_move(const SgfNode& node, Variant variant, Color& c, + MovePoints& points) +{ + string id; + // Pentobi 0.1 used BLUE/YELLOW/RED/GREEN instead of 1/2/3/4 as suggested + // by SGF FF[5]. Pentobi 12.0 erroneosly used 1/2 for two-player Callisto + // instead of B/W. We still want to be able to read files written by older + // versions. They will be converted to the current format by + // PentobiTreeWriter. + if (get_nu_colors(variant) == 2) + { + if (node.has_property("B")) + { + id = "B"; + c = Color(0); + } + else if (node.has_property("W")) + { + id = "W"; + c = Color(1); + } + else if (node.has_property("1")) + { + id = "1"; + c = Color(0); + } + else if (node.has_property("2")) + { + id = "2"; + c = Color(1); + } + else if (node.has_property("BLUE")) + { + id = "BLUE"; + c = Color(0); + } + else if (node.has_property("GREEN")) + { + id = "GREEN"; + c = Color(1); + } + } + else + { + if (node.has_property("1")) + { + id = "1"; + c = Color(0); + } + else if (node.has_property("2")) + { + id = "2"; + c = Color(1); + } + else if (node.has_property("3")) + { + id = "3"; + c = Color(2); + } + else if (node.has_property("4")) + { + id = "4"; + c = Color(3); + } + else if (node.has_property("BLUE")) + { + id = "BLUE"; + c = Color(0); + } + else if (node.has_property("YELLOW")) + { + id = "YELLOW"; + c = Color(1); + } + else if (node.has_property("RED")) + { + id = "RED"; + c = Color(2); + } + else if (node.has_property("GREEN")) + { + id = "GREEN"; + c = Color(3); + } + } + if (id.empty()) + return false; + vector values; + values = node.get_multi_property(id); + // Note: we still support having the points of a move in a list of point + // values instead of a single value as used by Pentobi <= 0.2, but it + // is deprecated + points.clear(); + auto& geo = get_geometry(variant); + bool is_nexos = (get_board_type(variant) == BoardType::nexos); + for (const auto& s : values) + { + if (trim(s).empty()) + continue; + vector v = split(s, ','); + for (const auto& p_str : v) + { + Point p; + if (! geo.from_string(p_str, p)) + throw InvalidPropertyValue(id, p_str); + if (is_nexos) + { + auto point_type = geo.get_point_type(p); + if (point_type != 1 && point_type != 2) + // Silently discard points that are not line segments, such + // files were written by some (unreleased) versions of + // Pentobi. + continue; + } + points.push_back(p); + } + } + return true; +} + +bool get_player(const SgfNode& node, Color& c) +{ + if (! node.has_property("PL")) + return false; + string value = node.get_property("PL"); + if (value == "B" || value == "1") + c = Color(0); + else if (value == "W" || value == "2") + c = Color(1); + else if (value == "3") + c = Color(2); + else if (value == "4") + c = Color(3); + else + throw InvalidTree("invalid value for PL property"); + return true; +} + +bool has_setup(const SgfNode& node) +{ + for (auto& i : node.get_properties()) + if (i.id == "AB" || i.id == "AW" || i.id == "A1" || i.id == "A2" + || i.id == "A3" || i.id == "A4" || i.id == "AE") + return true; + return false; +} + +//----------------------------------------------------------------------------- + +} // namespace node_util +} // namespace libpentobi_base diff --git a/src/libpentobi_base/NodeUtil.h b/src/libpentobi_base/NodeUtil.h new file mode 100644 index 0000000..dab324d --- /dev/null +++ b/src/libpentobi_base/NodeUtil.h @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/NodeUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_NODE_UTIL_H +#define LIBPENTOBI_BASE_NODE_UTIL_H + +#include "Color.h" +#include "MovePoints.h" +#include "Variant.h" +#include "libboardgame_sgf/SgfNode.h" + +namespace libpentobi_base { +namespace node_util { + +using libboardgame_sgf::SgfNode; + +//----------------------------------------------------------------------------- + +/** Get move points. + @param node + @param variant + @param[out] c The move color (only defined if return value is true) + @param[out] points The move points (only defined if return value is + true) + @return true if the node has a move property. */ +bool get_move(const SgfNode& node, Variant variant, Color& c, + MovePoints& points); + +/** Check if a node has setup properties (not including the PL property). */ +bool has_setup(const SgfNode& node); + +/** Get the color to play in a setup position (PL property). */ +bool get_player(const SgfNode& node, Color& c); + +//----------------------------------------------------------------------------- + +} // namespace node_util +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_NODE_UTIL_H diff --git a/src/libpentobi_base/PentobiSgfUtil.cpp b/src/libpentobi_base/PentobiSgfUtil.cpp new file mode 100644 index 0000000..6a09123 --- /dev/null +++ b/src/libpentobi_base/PentobiSgfUtil.cpp @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiSgfUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PentobiSgfUtil.h" + +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { +namespace sgf_util { + +//----------------------------------------------------------------------------- + +const char* get_color_id(Variant variant, Color c) +{ + static_assert(Color::range == 4, ""); + if (get_nu_colors(variant) == 2) + return c == Color(0) ? "B" : "W"; + if (c == Color(0)) + return "1"; + if (c == Color(1)) + return "2"; + if (c == Color(2)) + return "3"; + LIBBOARDGAME_ASSERT(c == Color(3)); + return "4"; +} + +const char* get_setup_id(Variant variant, Color c) +{ + static_assert(Color::range == 4, ""); + if (get_nu_colors(variant) == 2) + return c == Color(0) ? "AB" : "AW"; + if (c == Color(0)) + return "A1"; + if (c == Color(1)) + return "A2"; + if (c == Color(2)) + return "A3"; + LIBBOARDGAME_ASSERT(c == Color(3)); + return "A4"; +} + +//----------------------------------------------------------------------------- + +} // namespace sgf_util +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PentobiSgfUtil.h b/src/libpentobi_base/PentobiSgfUtil.h new file mode 100644 index 0000000..dc6111e --- /dev/null +++ b/src/libpentobi_base/PentobiSgfUtil.h @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiSgfUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PENTOBI_SGF_UTIL_H +#define LIBPENTOBI_BASE_PENTOBI_SGF_UTIL_H + +#include "Color.h" +#include "Variant.h" + +namespace libpentobi_base { +namespace sgf_util { + +//----------------------------------------------------------------------------- + +/** Get SGF move property ID for a color in a game variant. */ +const char* get_color_id(Variant variant, Color c); + +/** Get SGF setup property ID for a color in a game variant. */ +const char* get_setup_id(Variant variant, Color c); + +//----------------------------------------------------------------------------- + +} // namespace sgf_util +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PENTOBI_SGF_UTIL_H diff --git a/src/libpentobi_base/PentobiTree.cpp b/src/libpentobi_base/PentobiTree.cpp new file mode 100644 index 0000000..b684b5e --- /dev/null +++ b/src/libpentobi_base/PentobiTree.cpp @@ -0,0 +1,365 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiTree.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PentobiTree.h" + +#include "BoardUpdater.h" +#include "BoardUtil.h" +#include "NodeUtil.h" +#include "libboardgame_util/StringUtil.h" + +namespace libpentobi_base { + +using libboardgame_sgf::InvalidPropertyValue; +using libboardgame_sgf::InvalidTree; +using libboardgame_util::to_string; +using libpentobi_base::boardutil::get_current_position_as_setup; + +//----------------------------------------------------------------------------- + +PentobiTree::PentobiTree(Variant variant) +{ + init_variant(variant); +} + +PentobiTree::PentobiTree(unique_ptr& root) +{ + init(root); +} + +const SgfNode& PentobiTree::add_setup(const SgfNode& node, Color c, Move mv) +{ + const SgfNode* result; + if (has_move(node)) + result = &create_new_child(node); + else + result = &node; + Setup::PlacementList add_empty = get_setup_property(*result, "AE"); + if (add_empty.remove(mv)) + set_setup_property(*result, "AE", add_empty); + auto id = get_setup_prop_id(c); + Setup::PlacementList add_color = get_setup_property(*result, id); + if (add_color.include(mv)) + set_setup_property(*result, id, add_color); + return *result; +} + +const SgfNode* PentobiTree::find_child_with_move(const SgfNode& node, + ColorMove mv) const +{ + for (auto& i : node.get_children()) + if (get_move(i) == mv) + return &i; + return nullptr; +} + +ColorMove PentobiTree::get_move(const SgfNode& node) const +{ + Color c; + MovePoints points; + if (! libpentobi_base::node_util::get_move(node, m_variant, c, points)) + return ColorMove::null(); + if (points.size() == 0) + // Older (unreleased?) versions of Pentobi used empty move values + // to encode pass moves in search tree dumps but we don't support + // pass moves Board anymore. + return ColorMove::null(); + Move mv; + if (! m_bc->find_move(points, mv)) + throw InvalidTree("Tree contains illegal move"); + return ColorMove(c, mv); +} + +ColorMove PentobiTree::get_move_ignore_invalid(const SgfNode& node) const +{ + try + { + return get_move(node); + } + catch (const InvalidTree&) + { + return ColorMove::null(); + } +} + +const SgfNode* PentobiTree::get_node_before_move_number( + unsigned move_number) const +{ + auto node = &get_root(); + unsigned n = 0; + while (node->has_children()) + { + auto& child = node->get_first_child(); + if (! get_move(child).is_null() && n++ == move_number) + return node; + node = &child; + } + return nullptr; +} + +string PentobiTree::get_player_name(Color c) const +{ + string name; + auto& root = get_root(); + if (get_nu_players(m_variant) == 2) + { + if (c == Color(0) || c == Color(2)) + name = root.get_property("PB", ""); + else if (c == Color(1) || c == Color(2)) + name = root.get_property("PW", ""); + } + else + { + if (c == Color(0)) + name = root.get_property("P1", ""); + else if (c == Color(1)) + name = root.get_property("P2", ""); + else if (c == Color(2)) + name = root.get_property("P3", ""); + else if (c == Color(3)) + name = root.get_property("P4", ""); + } + return name; +} + +Setup::PlacementList PentobiTree::get_setup_property(const SgfNode& node, + const char* id) const +{ + vector values = node.get_multi_property(id); + Setup::PlacementList result; + for (const string& s : values) + result.push_back(m_bc->from_string(s)); + return result; +} + +Variant PentobiTree::get_variant(const SgfNode& root) +{ + string game = root.get_property("GM"); + Variant variant; + if (! parse_variant(game, variant)) + throw InvalidPropertyValue("GM", game); + return variant; +} + +bool PentobiTree::has_main_variation_moves() const +{ + auto node = &get_root(); + while (node) + { + if (has_move_ignore_invalid(*node)) + return true; + node = node->get_first_child_or_null(); + } + return false; +} + +void PentobiTree::init(unique_ptr& root) +{ + Variant variant = get_variant(*root); + SgfTree::init(root); + m_variant = variant; + init_board_const(variant); +} + +void PentobiTree::init_board_const(Variant variant) +{ + m_bc = &BoardConst::get(variant); +} + +void PentobiTree::init_variant(Variant variant) +{ + SgfTree::init(); + m_variant = variant; + set_game_property(); + init_board_const(variant); + clear_modified(); +} + +void PentobiTree::keep_only_subtree(const SgfNode& node) +{ + LIBBOARDGAME_ASSERT(contains(node)); + if (&node == &get_root()) + return; + string charset = get_root().get_property("CA", ""); + string application = get_root().get_property("AP", ""); + bool create_new_setup = has_move(node); + if (! create_new_setup) + { + auto current = node.get_parent_or_null(); + while (current) + { + if (has_move(*current) || node_util::has_setup(*current)) + { + create_new_setup = true; + break; + } + current = current->get_parent_or_null(); + } + } + if (create_new_setup) + { + unique_ptr bd(new Board(m_variant)); + BoardUpdater updater; + updater.update(*bd, *this, node); + Setup setup; + get_current_position_as_setup(*bd, setup); + LIBBOARDGAME_ASSERT(! node_util::has_setup(node)); + set_setup(node, setup); + } + make_root(node); + if (! application.empty()) + { + set_property(node, "AP", application); + move_property_to_front(node, "AP"); + } + if (! charset.empty()) + { + set_property(node, "CA", charset); + move_property_to_front(node, "CA"); + } + set_game_property(); +} + +void PentobiTree::remove_player(const SgfNode& node) +{ + remove_property(node, "PL"); +} + +const SgfNode& PentobiTree::remove_setup(const SgfNode& node, Color c, + Move mv) +{ + const SgfNode* result; + if (has_move(node)) + result = &create_new_child(node); + else + result = &node; + auto id = get_setup_prop_id(c); + auto add_color = get_setup_property(*result, id); + if (add_color.remove(mv)) + set_setup_property(*result, id, add_color); + else + { + Setup::PlacementList add_empty = get_setup_property(*result, "AE"); + if (add_empty.include(mv)) + set_setup_property(*result, "AE", add_empty); + } + return *result; +} + +void PentobiTree::set_game_property() +{ + auto& root = get_root(); + set_property(root, "GM", to_string(m_variant)); + move_property_to_front(root, "GM"); +} + +void PentobiTree::set_move(const SgfNode& node, Color c, Move mv) +{ + LIBBOARDGAME_ASSERT(! mv.is_null()); + auto id = get_color(c); + set_property(node, id, m_bc->to_string(mv, false)); +} + +void PentobiTree::set_player(const SgfNode& node, Color c) +{ + set_property(node, "PL", get_color(c)); +} + +void PentobiTree::set_player_name(Color c, const string& name) +{ + auto& root = get_root(); + if (get_nu_players(m_variant) == 2) + { + if (c == Color(0) || c == Color(2)) + set_property_remove_empty(root, "PB", name); + else if (c == Color(1) || c == Color(3)) + set_property_remove_empty(root, "PW", name); + } + else + { + if (c == Color(0)) + set_property_remove_empty(root, "P1", name); + else if (c == Color(1)) + set_property_remove_empty(root, "P2", name); + else if (c == Color(2)) + set_property_remove_empty(root, "P3", name); + else if (c == Color(3)) + set_property_remove_empty(root, "P4", name); + } +} + +void PentobiTree::set_result(const SgfNode& node, int score) +{ + if (score > 0) + { + ostringstream s; + s << "B+" << score; + set_property(node, "RE", s.str()); + } + else if (score < 0) + { + ostringstream s; + s << "W+" << (-score); + set_property(node, "RE", s.str()); + } + else + set_property(node, "RE", "0"); +} + +void PentobiTree::set_setup(const SgfNode& node, const Setup& setup) +{ + auto nu_colors = get_nu_colors(m_variant); + LIBBOARDGAME_ASSERT(nu_colors >= 2 && nu_colors <= 4); + remove_property(node, "B"); + remove_property(node, "W"); + remove_property(node, "1"); + remove_property(node, "2"); + remove_property(node, "3"); + remove_property(node, "4"); + remove_property(node, "AB"); + remove_property(node, "AW"); + remove_property(node, "A1"); + remove_property(node, "A2"); + remove_property(node, "A3"); + remove_property(node, "A4"); + remove_property(node, "AE"); + if (nu_colors == 2) + { + set_setup_property(node, "AB", setup.placements[Color(0)]); + set_setup_property(node, "AW", setup.placements[Color(1)]); + } + else + { + set_setup_property(node, "A1", setup.placements[Color(0)]); + set_setup_property(node, "A2", setup.placements[Color(1)]); + set_setup_property(node, "A3", setup.placements[Color(2)]); + if (nu_colors > 3) + set_setup_property(node, "A4", setup.placements[Color(3)]); + } + set_player(node, setup.to_play); +} + +void PentobiTree::set_setup_property(const SgfNode& node, const char* id, + const Setup::PlacementList& placements) +{ + if (placements.empty()) + { + remove_property(node, id); + return; + } + vector values; + for (Move mv : placements) + values.push_back(m_bc->to_string(mv, false)); + set_property(node, id, values); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PentobiTree.h b/src/libpentobi_base/PentobiTree.h new file mode 100644 index 0000000..213280a --- /dev/null +++ b/src/libpentobi_base/PentobiTree.h @@ -0,0 +1,168 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiTree.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PENTOBI_TREE_H +#define LIBPENTOBI_BASE_PENTOBI_TREE_H + +#include "ColorMove.h" +#include "BoardConst.h" +#include "Variant.h" +#include "Setup.h" +#include "PentobiSgfUtil.h" +#include "libboardgame_sgf/SgfTree.h" + +namespace libpentobi_base { + +using namespace std; +using libboardgame_sgf::SgfNode; +using libboardgame_sgf::SgfTree; + +//----------------------------------------------------------------------------- + +/** Blokus SGF tree. + See also doc/blksgf/Pentobi-SGF.html in the Pentobi distribution for + a description of the properties used. */ +class PentobiTree + : public SgfTree +{ +public: + /** Parse the GM property of a root node. + @throws MissingProperty + @throws InvalidPropertyValue */ + static Variant get_variant(const SgfNode& root); + + + explicit PentobiTree(Variant variant); + + explicit PentobiTree(unique_ptr& root); + + void init(unique_ptr& root) override; + + void init_variant(Variant variant); + + void set_move(const SgfNode& node, ColorMove mv); + + void set_move(const SgfNode& node, Color c, Move mv); + + /** Return move or ColorMove::null() if node has no move property. + @throws InvalidTree if the node has a move property with an invalid + value. */ + ColorMove get_move(const SgfNode& node) const; + + /** Like get_move() but returns ColorMove::null() on invalid property + value. */ + ColorMove get_move_ignore_invalid(const SgfNode& node) const; + + /** Same as ! get_move.is_null() */ + bool has_move(const SgfNode& node) const; + + /** Same as ! get_move_ignore_invalid.is_null() */ + bool has_move_ignore_invalid(const SgfNode& node) const; + + const SgfNode* find_child_with_move(const SgfNode& node, + ColorMove mv) const; + + void set_result(const SgfNode& node, int score); + + const SgfNode* get_node_before_move_number(unsigned move_number) const; + + Variant get_variant() const; + + string get_player_name(Color c) const; + + void set_player_name(Color c, const string& name); + + const BoardConst& get_board_const() const; + + /** Check if any node in the main variation has a move. + Invalid move properties are ignored. */ + bool has_main_variation_moves() const; + + void keep_only_subtree(const SgfNode& node); + + /** Add a piece as setup. + @pre ! mv.is_null() + If the node already contains a move, a new child will be created. + @pre The piece points must be empty on the board + @return The node or the new child if one was created. */ + const SgfNode& add_setup(const SgfNode& node, Color c, Move mv); + + /** Remove a piece using setup properties. + @pre ! mv.is_null() + If the node already contains a move, a new child will be created. + @pre The move must exist on the board + @return The node or the new child if one was created. */ + const SgfNode& remove_setup(const SgfNode& node, Color c, Move mv); + + /** Set the color to play in a setup position (PL property). */ + void set_player(const SgfNode& node, Color c); + + /** Remove the PL property. + @see set_player() */ + void remove_player(const SgfNode& node); + +private: + Variant m_variant; + + const BoardConst* m_bc; + + const char* get_color(Color c) const; + + Setup::PlacementList get_setup_property(const SgfNode& node, + const char* id) const; + + const char* get_setup_prop_id(Color c) const; + + void set_setup(const SgfNode& node, const Setup& setup); + + void init_board_const(Variant variant); + + void set_game_property(); + + void set_setup_property(const SgfNode& node, const char* id, + const Setup::PlacementList& placements); +}; + +inline const BoardConst& PentobiTree::get_board_const() const +{ + return *m_bc; +} + +inline const char* PentobiTree::get_color(Color c) const +{ + return sgf_util::get_color_id(m_variant, c); +} + +inline const char* PentobiTree::get_setup_prop_id(Color c) const +{ + return sgf_util::get_setup_id(m_variant, c); +} + +inline Variant PentobiTree::get_variant() const +{ + return m_variant; +} + +inline bool PentobiTree::has_move(const SgfNode& node) const +{ + return ! get_move(node).is_null(); +} + +inline bool PentobiTree::has_move_ignore_invalid(const SgfNode& node) const +{ + return ! get_move_ignore_invalid(node).is_null(); +} + +inline void PentobiTree::set_move(const SgfNode& node, ColorMove mv) +{ + set_move(node, mv.color, mv.move); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PENTOBI_SGF_TREE_H diff --git a/src/libpentobi_base/PentobiTreeWriter.cpp b/src/libpentobi_base/PentobiTreeWriter.cpp new file mode 100644 index 0000000..660b4b3 --- /dev/null +++ b/src/libpentobi_base/PentobiTreeWriter.cpp @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiTreeWriter.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PentobiTreeWriter.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +PentobiTreeWriter::PentobiTreeWriter(ostream& out, const PentobiTree& tree) + : libboardgame_sgf::TreeWriter(out, tree.get_root()), + m_variant(tree.get_variant()) +{ +} + +PentobiTreeWriter::~PentobiTreeWriter() +{ +} + +void PentobiTreeWriter::write_property(const string& id, + const vector& values) +{ + auto nu_colors = get_nu_colors(m_variant); + // Replace obsolete move property IDs or multi-valued move properties + // as used by early versions of Pentobi + if (id == "BLUE" || id == "YELLOW" || id == "GREEN" || id == "RED" + || ((id == "1" || id == "2" || id == "3" || id == "4" || id == "B" + || id == "W") + && values.size() > 1)) + { + string new_id; + if (id == "BLUE") + new_id = (nu_colors == 2 ? "B" : "1"); + else if (id == "YELLOW") + new_id = "2"; + else if (id == "GREEN") + new_id = (nu_colors == 2 ? "W" : "4"); + else if (id == "RED") + new_id = "3"; + else + new_id = id; + if (values.size() < 2) + libboardgame_sgf::TreeWriter::write_property(new_id, values); + else + { + string val = values[0]; + for (size_t i = 1; i < values.size(); ++i) + val += "," + values[i]; + vector new_values; + new_values.push_back(val); + libboardgame_sgf::TreeWriter::write_property(new_id, new_values); + } + return; + } + // Pentobi 12.0 versions erroneously used multi-player properties for + // two-player Callisto. + if (nu_colors == 2) + { + if (id == "1") + { + libboardgame_sgf::TreeWriter::write_property("B", values); + return; + } + if (id == "2") + { + libboardgame_sgf::TreeWriter::write_property("W", values); + return; + } + } + libboardgame_sgf::TreeWriter::write_property(id, values); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PentobiTreeWriter.h b/src/libpentobi_base/PentobiTreeWriter.h new file mode 100644 index 0000000..bd463a1 --- /dev/null +++ b/src/libpentobi_base/PentobiTreeWriter.h @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiTreeWriter.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PENTOBI_TREE_WRITER_H +#define LIBPENTOBI_BASE_PENTOBI_TREE_WRITER_H + +#include "PentobiTree.h" +#include "libboardgame_sgf/TreeWriter.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Blokus-specific tree writer. + Automatically replaces obsolete move properties as used by early versions + of Pentobi. */ +class PentobiTreeWriter + : public libboardgame_sgf::TreeWriter +{ +public: + PentobiTreeWriter(ostream& out, const PentobiTree& tree); + + virtual ~PentobiTreeWriter(); + + void write_property(const string& id, + const vector& values) override; + +private: + Variant m_variant; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PENTOBI_TREE_WRITER_H diff --git a/src/libpentobi_base/Piece.h b/src/libpentobi_base/Piece.h new file mode 100644 index 0000000..1aa3a29 --- /dev/null +++ b/src/libpentobi_base/Piece.h @@ -0,0 +1,110 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Piece.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PIECE_H +#define LIBPENTOBI_BASE_PIECE_H + +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Wrapper around an integer representing a piece type in a certain + game variant. */ +class Piece +{ +public: + typedef uint_fast8_t IntType; + + /** Maximum number of unique pieces per color. */ + static const IntType max_pieces = 24; + + /** Integer range used for unique pieces without the null piece. */ + static const IntType range_not_null = max_pieces; + + /** Integer range used for unique pieces including the null piece */ + static const IntType range = max_pieces + 1; + + static Piece null(); + + Piece(); + + explicit Piece(IntType i); + + bool operator==(const Piece& piece) const; + + bool operator!=(const Piece& piece) const; + + bool is_null() const; + + /** Return move as an integer between 0 and Piece::range */ + IntType to_int() const; + +private: + static const IntType value_null = range - 1; + + static const IntType value_uninitialized = range; + + IntType m_i; + + bool is_initialized() const; +}; + +inline Piece::Piece() +{ +#if LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +inline Piece::Piece(IntType i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = i; +} + +inline bool Piece::operator==(const Piece& piece) const +{ + return m_i == piece.m_i; +} + +inline bool Piece::operator!=(const Piece& piece) const +{ + return ! operator==(piece); +} + +inline bool Piece::is_initialized() const +{ + return m_i < value_uninitialized; +} + +inline bool Piece::is_null() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i == value_null; +} + +inline Piece Piece::null() +{ + return Piece(value_null); +} + +inline auto Piece::to_int() const -> IntType +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_PIECE_H diff --git a/src/libpentobi_base/PieceInfo.cpp b/src/libpentobi_base/PieceInfo.cpp new file mode 100644 index 0000000..663fb35 --- /dev/null +++ b/src/libpentobi_base/PieceInfo.cpp @@ -0,0 +1,231 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceInfo.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PieceInfo.h" + +#include +#include "libboardgame_base/GeometryUtil.h" +#include "libboardgame_util/Assert.h" +#include "libboardgame_util/Log.h" + +namespace libpentobi_base { + +using libboardgame_base::geometry_util::normalize_offset; +using libboardgame_base::geometry_util::type_match_shift; + +//----------------------------------------------------------------------------- + +namespace { + +const bool log_piece_creation = false; + +struct NormalizedPoints +{ + /** The normalized points of the transformed piece. + The points were shifted using GeometryUtil::normalize_offset(). */ + PiecePoints points; + + /** The point type of (0,0) in the normalized points. */ + unsigned point_type; + + bool operator==(const NormalizedPoints& n) const + { + return points == n.points && point_type == n.point_type; + } +}; + +#if LIBBOARDGAME_DEBUG +/** Check consistency of transformations. + Checks that the point list (which must be already sorted) has no + duplicates. */ +bool check_consistency(const PiecePoints& points) +{ + for (unsigned i = 0; i < points.size(); ++i) + if (i > 0 && points[i] == points[i - 1]) + return false; + return true; +} +#endif // LIBBOARDGAME_DEBUG + +/** Bring piece points into a normal form that is constant under translation. */ +NormalizedPoints normalize(const PiecePoints& points, unsigned point_type, + const Geometry& geo) +{ + if (log_piece_creation) + LIBBOARDGAME_LOG("Points ", points); + NormalizedPoints normalized; + normalized.points = points; + type_match_shift(geo, normalized.points.begin(), + normalized.points.end(), point_type); + if (log_piece_creation) + LIBBOARDGAME_LOG("Point type ", point_type, ", type match shift ", + normalized.points); + // Make the coordinates positive and minimal + unsigned width; // unused + unsigned height; // unused + CoordPoint offset; + normalize_offset(normalized.points.begin(), normalized.points.end(), + width, height, offset); + normalized.point_type = geo.get_point_type(offset); + // Sort the coordinates + sort(normalized.points.begin(), normalized.points.end()); + return normalized; +} + +} // namespace + +//----------------------------------------------------------------------------- + +PieceInfo::PieceInfo(const string& name, const PiecePoints& points, + const Geometry& geo, const PieceTransforms& transforms, + PieceSet piece_set, const CoordPoint& label_pos, + unsigned nu_instances) + : m_nu_instances(nu_instances), + m_points(points), + m_label_pos(label_pos), + m_transforms(&transforms), + m_name(name) +{ + LIBBOARDGAME_ASSERT(nu_instances > 0); + LIBBOARDGAME_ASSERT(nu_instances <= PieceInfo::max_instances); + if (log_piece_creation) + LIBBOARDGAME_LOG("Creating transformations for piece ", name, ' ', + points); + vector all_transformed_points; + PiecePoints transformed_points; + for (const Transform* transform : transforms.get_all()) + { + if (log_piece_creation) + LIBBOARDGAME_LOG("Transformation ", typeid(*transform).name()); + transformed_points = points; + transform->transform(transformed_points.begin(), + transformed_points.end()); + NormalizedPoints normalized = normalize(transformed_points, + transform->get_new_point_type(), + geo); + if (log_piece_creation) + LIBBOARDGAME_LOG("Normalized ", normalized.points, " point type ", + normalized.point_type); + LIBBOARDGAME_ASSERT(check_consistency(normalized.points)); + auto begin = all_transformed_points.begin(); + auto end = all_transformed_points.end(); + auto pos = find(begin, end, normalized); + if (pos != end) + { + if (log_piece_creation) + LIBBOARDGAME_LOG("Equivalent to ", pos - begin); + m_equivalent_transform[transform] + = transforms.get_all()[pos - begin]; + } + else + { + if (log_piece_creation) + LIBBOARDGAME_LOG("New (", m_uniq_transforms.size(), ")"); + m_equivalent_transform[transform] = transform; + m_uniq_transforms.push_back(transform); + } + all_transformed_points.push_back(normalized); + }; + if (piece_set == PieceSet::nexos) + { + m_score_points = 0; + for (auto& p : points) + { + auto point_type = geo.get_point_type(p); + LIBBOARDGAME_ASSERT(point_type <= 2); + if (point_type == 1 || point_type == 2) // Line segment + ++m_score_points; + } + } + else if (points.size() == 1 && piece_set == PieceSet::callisto) + m_score_points = 0; + else + m_score_points = static_cast(points.size()); +} + +bool PieceInfo::can_flip_horizontally(const Transform* transform) const +{ + transform = get_equivalent_transform(transform); + auto flip = get_equivalent_transform( + m_transforms->get_mirrored_horizontally(transform)); + return flip != transform; +} + +bool PieceInfo::can_flip_vertically(const Transform* transform) const +{ + transform = get_equivalent_transform(transform); + auto flip = get_equivalent_transform( + m_transforms->get_mirrored_vertically(transform)); + return flip != transform; +} + +bool PieceInfo::can_rotate() const +{ + auto transform = m_uniq_transforms[0]; + auto rotate = get_equivalent_transform( + m_transforms->get_rotated_clockwise(transform)); + return rotate != transform; +} + +const Transform* PieceInfo::find_transform(const Geometry& geo, + const Points& points) const +{ + NormalizedPoints normalized = + normalize(points, geo.get_point_type(0, 0), geo); + for (const Transform* transform : get_transforms()) + { + Points piece_points = get_points(); + transform->transform(piece_points.begin(), piece_points.end()); + NormalizedPoints normalized_piece = + normalize(piece_points, transform->get_new_point_type(), geo); + if (normalized_piece == normalized) + return transform; + } + return nullptr; +} + +const Transform* PieceInfo::get_equivalent_transform( + const Transform* transform) const +{ + auto pos = m_equivalent_transform.find(transform); + LIBBOARDGAME_ASSERT(pos != m_equivalent_transform.end()); + return pos->second; +} + +const Transform* PieceInfo::get_next_transform(const Transform* transform) const +{ + transform = get_equivalent_transform(transform); + auto begin = m_uniq_transforms.begin(); + auto end = m_uniq_transforms.end(); + auto pos = find(begin, end, transform); + LIBBOARDGAME_ASSERT(pos != end); + if (pos + 1 == end) + return *begin; + else + return *(pos + 1); +} + +const Transform* PieceInfo::get_previous_transform( + const Transform* transform) const +{ + transform = get_equivalent_transform(transform); + auto begin = m_uniq_transforms.begin(); + auto end = m_uniq_transforms.end(); + auto pos = find(begin, end, transform); + LIBBOARDGAME_ASSERT(pos != end); + if (pos == begin) + return *(end - 1); + else + return *(pos - 1); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PieceInfo.h b/src/libpentobi_base/PieceInfo.h new file mode 100644 index 0000000..416fb5f --- /dev/null +++ b/src/libpentobi_base/PieceInfo.h @@ -0,0 +1,137 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceInfo.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PIECE_INFO_H +#define LIBPENTOBI_BASE_PIECE_INFO_H + +#include +#include +#include +#include "Geometry.h" +#include "PieceTransforms.h" +#include "Variant.h" +#include "libboardgame_base/CoordPoint.h" +#include "libboardgame_base/Transform.h" +#include "libboardgame_util/ArrayList.h" + +namespace libpentobi_base { + +using namespace std; +using libboardgame_base::CoordPoint; +using libboardgame_base::Transform; +using libboardgame_util::ArrayList; + +//----------------------------------------------------------------------------- + +typedef float ScoreType; + +//----------------------------------------------------------------------------- + +class PieceInfo +{ +public: + /** Maximum number of points in a piece. + The maximum piece size occurs with the I4 piece in Nexos (4 real points + and 3 junction points, see get_points()). */ + static const unsigned max_size = 7; + + /** Maximum number of scored points in a piece. + This excludes junction points in Nexos. The maximum number of scored + points occurs in Trigon. */ + static const unsigned max_scored_size = 6; + + /** Maximum number of instances of a piece per player. */ + static const unsigned max_instances = 3; + + typedef ArrayList Points; + + + /** Constructor. + @param name A short unique name for the piece. + @param points The coordinates of the piece elements. + @param geo + @param transforms + @param piece_set + @param label_pos The coordinates for drawing a label on the piece. + @param nu_instances The number of instances of the piece per player. */ + PieceInfo(const string& name, const Points& points, + const Geometry& geo, const PieceTransforms& transforms, + PieceSet piece_set, const CoordPoint& label_pos, + unsigned nu_instances = 1); + + const string& get_name() const { return m_name; } + + /** The points of the piece. + In Nexos, the points of a piece contain the coordinates of line + segments and of junctions that are essentially needed to mark the + intersection as non-crossable (i.e. junctions that touch exactly two + line segments of the piece with identical orientation. */ + const Points& get_points() const { return m_points; } + + const CoordPoint& get_label_pos() const { return m_label_pos; } + + /** Return the number of points of the piece that contribute to the score. + This excludes any junction points included in the piece definition in + Nexos.*/ + ScoreType get_score_points() const { return m_score_points; } + + unsigned get_nu_instances() const { return m_nu_instances; } + + /** Get a list with unique transformations. + The list has the same order as PieceTransforms::get_all() but + transformations that are equivalent to a previous transformation + (because of a symmetry of the piece) are omitted. */ + const vector& get_transforms() const + { + return m_uniq_transforms; + } + + /** Get next transform from the list of unique transforms. */ + const Transform* get_next_transform(const Transform* transform) const; + + /** Get previous transform from the list of unique transforms. */ + const Transform* get_previous_transform(const Transform* transform) const; + + /** Get the transform from the list of unique transforms that is equivalent + to a given transform. */ + const Transform* get_equivalent_transform(const Transform* transform) const; + + bool can_rotate() const; + + bool can_flip_horizontally(const Transform* transform) const; + + bool can_flip_vertically(const Transform* transform) const; + + const Transform* find_transform(const Geometry& geo, + const Points& points) const; + +private: + unsigned m_nu_instances; + + Points m_points; + + CoordPoint m_label_pos; + + ScoreType m_score_points; + + const PieceTransforms* m_transforms; + + string m_name; + + vector m_uniq_transforms; + + map m_equivalent_transform; +}; + +//----------------------------------------------------------------------------- + +typedef PieceInfo::Points PiecePoints; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PIECE_INFO_H diff --git a/src/libpentobi_base/PieceMap.h b/src/libpentobi_base/PieceMap.h new file mode 100644 index 0000000..67930c6 --- /dev/null +++ b/src/libpentobi_base/PieceMap.h @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceMap.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PIECE_MAP_H +#define LIBPENTOBI_BASE_PIECE_MAP_H + +#include +#include +#include "Piece.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Container mapping a unique piece to another element type. + The elements must be default-constructible. */ +template +class PieceMap +{ +public: + PieceMap() = default; + + explicit PieceMap(const T& val); + + PieceMap& operator=(const PieceMap& piece_map); + + bool operator==(const PieceMap& piece_map) const; + + T& operator[](Piece piece); + + const T& operator[](Piece piece) const; + + void fill(const T& val); + +private: + array m_a; +}; + +template +inline PieceMap::PieceMap(const T& val) +{ + fill(val); +} + +template +PieceMap& PieceMap::operator=(const PieceMap& piece_map) +{ + copy(piece_map.m_a.begin(), piece_map.m_a.end(), m_a.begin()); + return *this; +} + +template +bool PieceMap::operator==(const PieceMap& piece_map) const +{ + return equal(m_a.begin(), m_a.end(), piece_map.m_a.begin()); +} + +template +inline T& PieceMap::operator[](Piece piece) +{ + LIBBOARDGAME_ASSERT(! piece.is_null()); + return m_a[piece.to_int()]; +} + +template +inline const T& PieceMap::operator[](Piece piece) const +{ + LIBBOARDGAME_ASSERT(! piece.is_null()); + return m_a[piece.to_int()]; +} + +template +void PieceMap::fill(const T& val) +{ + m_a.fill(val); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PIECE_MAP_H diff --git a/src/libpentobi_base/PieceTransforms.cpp b/src/libpentobi_base/PieceTransforms.cpp new file mode 100644 index 0000000..c67551e --- /dev/null +++ b/src/libpentobi_base/PieceTransforms.cpp @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransforms.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PieceTransforms.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +PieceTransforms::~PieceTransforms() = default; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PieceTransforms.h b/src/libpentobi_base/PieceTransforms.h new file mode 100644 index 0000000..da6a4cb --- /dev/null +++ b/src/libpentobi_base/PieceTransforms.h @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransforms.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_PIECE_TRANSFORMS_H +#define LIBPENTOBI_PIECE_TRANSFORMS_H + +#include +#include "libboardgame_base/Transform.h" + +namespace libpentobi_base { + +using namespace std; +using libboardgame_base::Transform; + +//----------------------------------------------------------------------------- + +class PieceTransforms +{ +public: + virtual ~PieceTransforms(); + + virtual const Transform* get_mirrored_horizontally( + const Transform* transf) const = 0; + + virtual const Transform* get_mirrored_vertically( + const Transform* transf) const = 0; + + virtual const Transform* get_rotated_anticlockwise( + const Transform* transf) const = 0; + + virtual const Transform* get_rotated_clockwise( + const Transform* transf) const = 0; + + virtual const Transform* get_default() const; + + const vector& get_all() const; + + /** Find the transform by its class. + @tparam T The class of the transform. + @return The pointer to the transform or null if the transforms do not + contain the instance of the given class. */ + template + const Transform* find() const; + +protected: + /** All piece transformations. + Must be initialized in constructor of subclass. */ + vector m_all; +}; + +template +const Transform* PieceTransforms::find() const +{ + for (auto t : m_all) + if (dynamic_cast(t)) + return t; + return nullptr; +} + +inline const Transform* PieceTransforms::get_default() const +{ + return m_all[0]; +} + +inline const vector& PieceTransforms::get_all() const +{ + return m_all; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_PIECE_TRANSFORMS_H diff --git a/src/libpentobi_base/PieceTransformsClassic.cpp b/src/libpentobi_base/PieceTransformsClassic.cpp new file mode 100644 index 0000000..2186613 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsClassic.cpp @@ -0,0 +1,146 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransformsClassic.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PieceTransformsClassic.h" + +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +PieceTransformsClassic::PieceTransformsClassic() +{ + m_all.reserve(8); + m_all.push_back(&m_identity); + m_all.push_back(&m_rot90); + m_all.push_back(&m_rot180); + m_all.push_back(&m_rot270); + m_all.push_back(&m_refl); + m_all.push_back(&m_rot90refl); + m_all.push_back(&m_rot180refl); + m_all.push_back(&m_rot270refl); +} + +const Transform* PieceTransformsClassic::get_mirrored_horizontally( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_refl; + else if (transf == &m_rot90) + result = &m_rot270refl; + else if (transf == &m_rot180) + result = &m_rot180refl; + else if (transf == &m_rot270) + result = &m_rot90refl; + else if (transf == &m_refl) + result = &m_identity; + else if (transf == &m_rot90refl) + result = &m_rot270; + else if (transf == &m_rot180refl) + result = &m_rot180; + else if (transf == &m_rot270refl) + result = &m_rot90; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsClassic::get_mirrored_vertically( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_rot180refl; + else if (transf == &m_rot90) + result = &m_rot90refl; + else if (transf == &m_rot180) + result = &m_refl; + else if (transf == &m_rot270) + result = &m_rot270refl; + else if (transf == &m_refl) + result = &m_rot180; + else if (transf == &m_rot90refl) + result = &m_rot90; + else if (transf == &m_rot180refl) + result = &m_identity; + else if (transf == &m_rot270refl) + result = &m_rot270; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsClassic::get_rotated_anticlockwise( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_rot270; + else if (transf == &m_rot90) + result = &m_identity; + else if (transf == &m_rot180) + result = &m_rot90; + else if (transf == &m_rot270) + result = &m_rot180; + else if (transf == &m_refl) + result = &m_rot270refl; + else if (transf == &m_rot90refl) + result = &m_refl; + else if (transf == &m_rot180refl) + result = &m_rot90refl; + else if (transf == &m_rot270refl) + result = &m_rot180refl; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsClassic::get_rotated_clockwise( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_rot90; + else if (transf == &m_rot90) + result = &m_rot180; + else if (transf == &m_rot180) + result = &m_rot270; + else if (transf == &m_rot270) + result = &m_identity; + else if (transf == &m_refl) + result = &m_rot90refl; + else if (transf == &m_rot90refl) + result = &m_rot180refl; + else if (transf == &m_rot180refl) + result = &m_rot270refl; + else if (transf == &m_rot270refl) + result = &m_refl; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PieceTransformsClassic.h b/src/libpentobi_base/PieceTransformsClassic.h new file mode 100644 index 0000000..e466845 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsClassic.h @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransformsClassic.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_PIECE_TRANSFORMS_CLASSIC_H +#define LIBPENTOBI_PIECE_TRANSFORMS_CLASSIC_H + +#include "PieceTransforms.h" +#include "libboardgame_base/RectTransform.h" + +namespace libpentobi_base { + +using libboardgame_base::TransfIdentity; +using libboardgame_base::TransfRectRot90; +using libboardgame_base::TransfRectRot180; +using libboardgame_base::TransfRectRot270; +using libboardgame_base::TransfRectRefl; +using libboardgame_base::TransfRectRot90Refl; +using libboardgame_base::TransfRectRot180Refl; +using libboardgame_base::TransfRectRot270Refl; + +//----------------------------------------------------------------------------- + +class PieceTransformsClassic + : public PieceTransforms +{ +public: + PieceTransformsClassic(); + + const Transform* get_mirrored_horizontally( + const Transform* transf) const override; + + const Transform* get_mirrored_vertically( + const Transform* transf) const override; + + const Transform* get_rotated_anticlockwise( + const Transform* transf) const override; + + const Transform* get_rotated_clockwise( + const Transform* transf) const override; + +private: + TransfIdentity m_identity; + + TransfRectRot90 m_rot90; + + TransfRectRot180 m_rot180; + + TransfRectRot270 m_rot270; + + TransfRectRefl m_refl; + + TransfRectRot90Refl m_rot90refl; + + TransfRectRot180Refl m_rot180refl; + + TransfRectRot270Refl m_rot270refl; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_PIECE_TRANSFORMS_CLASSIC_H diff --git a/src/libpentobi_base/PieceTransformsTrigon.cpp b/src/libpentobi_base/PieceTransformsTrigon.cpp new file mode 100644 index 0000000..2b0f2d3 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsTrigon.cpp @@ -0,0 +1,187 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransformsTrigon.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PieceTransformsTrigon.h" + +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +PieceTransformsTrigon::PieceTransformsTrigon() +{ + m_all.reserve(12); + m_all.push_back(&m_identity); + m_all.push_back(&m_rot60); + m_all.push_back(&m_rot120); + m_all.push_back(&m_rot180); + m_all.push_back(&m_rot240); + m_all.push_back(&m_rot300); + m_all.push_back(&m_refl); + m_all.push_back(&m_refl_rot60); + m_all.push_back(&m_refl_rot120); + m_all.push_back(&m_refl_rot180); + m_all.push_back(&m_refl_rot240); + m_all.push_back(&m_refl_rot300); +} + +const Transform* PieceTransformsTrigon::get_default() const +{ + return &m_identity; +} + +const Transform* PieceTransformsTrigon::get_mirrored_horizontally( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_refl; + else if (transf == &m_rot60) + result = &m_refl_rot300; + else if (transf == &m_rot120) + result = &m_refl_rot240; + else if (transf == &m_rot180) + result = &m_refl_rot180; + else if (transf == &m_rot240) + result = &m_refl_rot120; + else if (transf == &m_rot300) + result = &m_refl_rot60; + else if (transf == &m_refl) + result = &m_identity; + else if (transf == &m_refl_rot60) + result = &m_rot300; + else if (transf == &m_refl_rot120) + result = &m_rot240; + else if (transf == &m_refl_rot180) + result = &m_rot180; + else if (transf == &m_refl_rot240) + result = &m_rot120; + else if (transf == &m_refl_rot300) + result = &m_rot60; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsTrigon::get_mirrored_vertically( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_refl_rot180; + else if (transf == &m_rot60) + result = &m_refl_rot120; + else if (transf == &m_rot120) + result = &m_refl_rot60; + else if (transf == &m_rot180) + result = &m_refl; + else if (transf == &m_rot240) + result = &m_refl_rot300; + else if (transf == &m_rot300) + result = &m_refl_rot240; + else if (transf == &m_refl) + result = &m_rot180; + else if (transf == &m_refl_rot60) + result = &m_rot120; + else if (transf == &m_refl_rot120) + result = &m_rot60; + else if (transf == &m_refl_rot180) + result = &m_identity; + else if (transf == &m_refl_rot240) + result = &m_rot300; + else if (transf == &m_refl_rot300) + result = &m_rot240; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsTrigon::get_rotated_anticlockwise( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_rot300; + else if (transf == &m_rot60) + result = &m_identity; + else if (transf == &m_rot120) + result = &m_rot60; + else if (transf == &m_rot180) + result = &m_rot120; + else if (transf == &m_rot240) + result = &m_rot180; + else if (transf == &m_rot300) + result = &m_rot240; + else if (transf == &m_refl) + result = &m_refl_rot300; + else if (transf == &m_refl_rot60) + result = &m_refl; + else if (transf == &m_refl_rot120) + result = &m_refl_rot60; + else if (transf == &m_refl_rot180) + result = &m_refl_rot120; + else if (transf == &m_refl_rot240) + result = &m_refl_rot180; + else if (transf == &m_refl_rot300) + result = &m_refl_rot240; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsTrigon::get_rotated_clockwise( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_rot60; + else if (transf == &m_rot60) + result = &m_rot120; + else if (transf == &m_rot120) + result = &m_rot180; + else if (transf == &m_rot180) + result = &m_rot240; + else if (transf == &m_rot240) + result = &m_rot300; + else if (transf == &m_rot300) + result = &m_identity; + else if (transf == &m_refl) + result = &m_refl_rot60; + else if (transf == &m_refl_rot60) + result = &m_refl_rot120; + else if (transf == &m_refl_rot120) + result = &m_refl_rot180; + else if (transf == &m_refl_rot180) + result = &m_refl_rot240; + else if (transf == &m_refl_rot240) + result = &m_refl_rot300; + else if (transf == &m_refl_rot300) + result = &m_refl; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PieceTransformsTrigon.h b/src/libpentobi_base/PieceTransformsTrigon.h new file mode 100644 index 0000000..47d94d0 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsTrigon.h @@ -0,0 +1,67 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransformsTrigon.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_PIECE_TRANSFORMS_TRIGON_H +#define LIBPENTOBI_PIECE_TRANSFORMS_TRIGON_H + +#include "PieceTransforms.h" +#include "TrigonTransform.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +class PieceTransformsTrigon + : public PieceTransforms +{ +public: + PieceTransformsTrigon(); + + const Transform* get_mirrored_horizontally( + const Transform* transf) const override; + + const Transform* get_mirrored_vertically( + const Transform* transf) const override; + + const Transform* get_rotated_anticlockwise( + const Transform* transf) const override; + + const Transform* get_rotated_clockwise( + const Transform* transf) const override; + + const Transform* get_default() const override; + +private: + TransfTrigonIdentity m_identity; + + TransfTrigonRot60 m_rot60; + + TransfTrigonRot120 m_rot120; + + TransfTrigonRot180 m_rot180; + + TransfTrigonRot240 m_rot240; + + TransfTrigonRot300 m_rot300; + + TransfTrigonRefl m_refl; + + TransfTrigonReflRot60 m_refl_rot60; + + TransfTrigonReflRot120 m_refl_rot120; + + TransfTrigonReflRot180 m_refl_rot180; + + TransfTrigonReflRot240 m_refl_rot240; + + TransfTrigonReflRot300 m_refl_rot300; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_PIECE_TRANSFORMS_TRIGON_H diff --git a/src/libpentobi_base/PlayerBase.cpp b/src/libpentobi_base/PlayerBase.cpp new file mode 100644 index 0000000..d6be112 --- /dev/null +++ b/src/libpentobi_base/PlayerBase.cpp @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PlayerBase.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PlayerBase.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +PlayerBase::~PlayerBase() +{ +} + +bool PlayerBase::resign() const +{ + return false; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PlayerBase.h b/src/libpentobi_base/PlayerBase.h new file mode 100644 index 0000000..df9b92a --- /dev/null +++ b/src/libpentobi_base/PlayerBase.h @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PlayerBase.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PLAYER_BASE_H +#define LIBPENTOBI_BASE_PLAYER_BASE_H + +#include "Board.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +class PlayerBase +{ +public: + virtual ~PlayerBase(); + + virtual Move genmove(const Board& bd, Color c) = 0; + + /** Check if the player wants to resign. + This may only be called after a genmove() and returns true if the + player wants to resign in the position at the last genmove(). + The default implementation returns false. */ + virtual bool resign() const; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PLAYER_BASE_H diff --git a/src/libpentobi_base/Point.h b/src/libpentobi_base/Point.h new file mode 100644 index 0000000..6b3dae9 --- /dev/null +++ b/src/libpentobi_base/Point.h @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Point.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_POINT_H +#define LIBPENTOBI_BASE_POINT_H + +#include "libboardgame_base/Point.h" + +//----------------------------------------------------------------------------- + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Point (coordinate of on-board field) for Blokus game variants. + Supports RectGeometry up to size 20, TrigonGeometry up to edge size 9, + and NexosGeometry up to size 13. */ +typedef libboardgame_base::Point<486, 35, 25, unsigned short> Point; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_POINT_H diff --git a/src/libpentobi_base/PointList.h b/src/libpentobi_base/PointList.h new file mode 100644 index 0000000..4cfab9e --- /dev/null +++ b/src/libpentobi_base/PointList.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PointList.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_POINT_LIST_H +#define LIBPENTOBI_BASE_POINT_LIST_H + +#include "Point.h" +#include "libboardgame_util/ArrayList.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +typedef ArrayList PointList; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_POINT_LIST_H diff --git a/src/libpentobi_base/PointState.cpp b/src/libpentobi_base/PointState.cpp new file mode 100644 index 0000000..de1267b --- /dev/null +++ b/src/libpentobi_base/PointState.cpp @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PointState.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PointState.h" + +#include + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const PointState& s) +{ + if (s.is_color()) + out << s.to_color(); + else + out << 'E'; + return out; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PointState.h b/src/libpentobi_base/PointState.h new file mode 100644 index 0000000..a950b51 --- /dev/null +++ b/src/libpentobi_base/PointState.h @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PointState.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_POINTSTATE_H +#define LIBPENTOBI_BASE_POINTSTATE_H + +#include "Color.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** State of an on-board point, which can be a color or empty */ +class PointState +{ +public: + typedef Color::IntType IntType; + + static const IntType range = Color::range + 1; + + static const IntType value_empty = range - 1; + + + PointState(); + + explicit PointState(Color c); + + explicit PointState(IntType i); + + bool operator==(const PointState& s) const; + + bool operator!=(const PointState& s) const; + + bool operator==(const Color& c) const; + + bool operator!=(const Color& c) const; + + IntType to_int() const; + + static PointState empty(); + + bool is_empty() const; + + bool is_color() const; + + Color to_color() const; + +private: + static const IntType value_uninitialized = range; + + IntType m_i; + + bool is_initialized() const; +}; + + +inline PointState::PointState() +{ +#if LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +inline PointState::PointState(Color c) +{ + m_i = c.to_int(); +} + +inline PointState::PointState(IntType i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = i; +} + +inline bool PointState::operator==(const PointState& p) const +{ + return m_i == p.m_i; +} + +inline bool PointState::operator==(const Color& c) const +{ + return m_i == c.to_int(); +} + +inline bool PointState::operator!=(const PointState& s) const +{ + return ! operator==(s); +} + +inline bool PointState::operator!=(const Color& c) const +{ + return ! operator==(c); +} + +inline PointState PointState::empty() +{ + return PointState(value_empty); +} + +inline bool PointState::is_initialized() const +{ + return m_i < value_uninitialized; +} + +inline bool PointState::is_color() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i != value_empty; +} + +inline bool PointState::is_empty() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i == value_empty; +} + +inline Color PointState::to_color() const +{ + LIBBOARDGAME_ASSERT(is_color()); + return Color(m_i); +} + +inline PointState::IntType PointState::to_int() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const PointState& s); + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_POINTSTATE_H diff --git a/src/libpentobi_base/PrecompMoves.h b/src/libpentobi_base/PrecompMoves.h new file mode 100644 index 0000000..60a05a9 --- /dev/null +++ b/src/libpentobi_base/PrecompMoves.h @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PrecompMoves.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PRECOMP_MOVES_H +#define LIBPENTOBI_BASE_PRECOMP_MOVES_H + +#include "Grid.h" +#include "Move.h" +#include "PieceMap.h" +#include "Point.h" +#include "libboardgame_util/Range.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Precomputed moves for fast move generation. + Compact storage of precomputed lists with local moves. Each list contains + all moves that include a given point constrained by the piece type and the + forbidden status of adjacant points. This drastically reduces the number of + moves that need to be checked for legality during move generation. + @see Board::get_adj_status() */ +class PrecompMoves +{ +public: + /** The number of neighbors used for computing the adjacent status. + The adjacent status is a single number that encodes the forbidden + status of the first adj_status_nu_adj neighbors (from the list + Geometry::get_adj() concatenated with Geometry::get_diag()). It is used + for speeding up the matching of moves at a given point. Increasing this + number will make the precomputed lists shorter but exponentially + increase the number of lists and the total memory used for all lists. + Therefore, the optimal value for speeding up the matching depends on + the CPU cache size. */ +#if PENTOBI_LOW_RESOURCES + static const unsigned adj_status_nu_adj = 5; +#else + static const unsigned adj_status_nu_adj = 6; +#endif + + /** The maximum sum of the sizes of all precomputed move lists in any + game variant. */ + static const unsigned max_move_lists_sum_length = + adj_status_nu_adj == 4 ? + 832444 : adj_status_nu_adj == 5 ? 1425934 : 2769060; + static_assert(adj_status_nu_adj >= 4 && adj_status_nu_adj <= 6, ""); + + /** The range of values for the adjacent status. */ + static const unsigned nu_adj_status = 1 << adj_status_nu_adj; + + /** Begin/end range for lists with moves at a given point. */ + typedef libboardgame_util::Range Range; + + + /** Add a move to list during construction. */ + void set_move(unsigned i, Move mv) + { + LIBBOARDGAME_ASSERT(i < max_move_lists_sum_length); + m_move_lists[i] = mv; + } + + /** Store beginning and end of a local move list duing construction. */ + void set_list_range(Point p, unsigned adj_status, Piece piece, + unsigned begin, unsigned size) + { + m_moves_range[p][adj_status][piece] = CompressedRange(begin, size); + } + + /** Get all moves of a piece at a point constrained by the forbidden + status of adjacent points. */ + Range get_moves(Piece piece, Point p, unsigned adj_status = 0) const + { + auto& range = m_moves_range[p][adj_status][piece]; + auto begin = move_lists_begin() + range.begin(); + auto end = begin + range.size(); + return Range(begin, end); + } + + bool has_moves(Piece piece, Point p, unsigned adj_status) const + { + return ! m_moves_range[p][adj_status][piece].empty(); + } + + /** Begin of storage for move lists. + Only needed for special use cases like during an in-place construction + of PrecompMoves for follow-up positions when we need to compare the + index of old iterators with the current get_size() to ensure that + we don't overwrite any old content that we still need to read + during the construction. */ + const Move* move_lists_begin() const { return &(*m_move_lists.begin()); } + +private: + class CompressedRange + { + public: + CompressedRange() = default; + + CompressedRange(unsigned begin, unsigned size) + { + LIBBOARDGAME_ASSERT(begin < max_move_lists_sum_length); + LIBBOARDGAME_ASSERT(begin + size <= max_move_lists_sum_length); + static_assert(max_move_lists_sum_length < (1 << 24), ""); + LIBBOARDGAME_ASSERT(size < (1 << 8)); + m_val = size; + if (size != 0) + m_val |= (begin << 8); + } + + bool empty() const { return m_val == 0; } + + unsigned begin() const { return m_val >> 8; } + + unsigned size() const { return m_val & 0xff; } + + private: + uint_least32_t m_val; + }; + + /** See m_move_lists. */ + Grid, nu_adj_status>> m_moves_range; + + /** Compact representation of lists of moves of a piece at a point + constrained by the forbidden status of adjacent points. + All lists are stored in a single array; m_moves_range contains + information about the actual begin/end indices. */ + array m_move_lists; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PRECOMP_MOVES_H diff --git a/src/libpentobi_base/ScoreUtil.h b/src/libpentobi_base/ScoreUtil.h new file mode 100644 index 0000000..a2a661e --- /dev/null +++ b/src/libpentobi_base/ScoreUtil.h @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/ScoreUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_SCORE_UTIL_H +#define LIBPENTOBI_BASE_SCORE_UTIL_H + +#include +#include +#include "Color.h" +#include "PieceInfo.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Convert the result of a multi-player game into a comparable number. + This generalizes the game result of a two-player game (0,0.5,1 for + loss/tie/win) for a game with n \> 2 players. The points are sorted in + ascending order. Each rank r_i (i in 0..n-1) is assigned a value of + r_i/(n-1). If multiple players have the same points, the result value is + the average of all ranks with these points. So being the single winner + still gives the result 1 and being the single loser the result 0. Being the + single winner is better than sharing the best rank, which is better than + getting the second rank, etc. + @return The game result for each player. */ +template +void get_multiplayer_result(unsigned nu_players, + const array& points, + array& result, + bool break_ties) +{ + array adjusted, sorted; + for (Color::IntType i = 0; i < nu_players; ++i) + { + adjusted[i] = points[i]; + if (break_ties) + // Favor later player. The adjustment must be smaller than the + // smallest difference in points (0.5 for GembloQ). + adjusted[i] += 0.001f * i; + sorted[i] = adjusted[i]; + } + sort(sorted.begin(), sorted.begin() + nu_players); + for (Color::IntType i = 0; i < nu_players; ++i) + { + FLOAT sum = 0; + FLOAT n = 0; + FLOAT float_j = 0; + FLOAT factor = 1 / FLOAT(nu_players - 1); + for (unsigned j = 0; j < nu_players; ++j, ++float_j) + if (sorted[j] == adjusted[i]) + { + sum += factor * float_j; + ++n; + } + result[i] = sum / n; + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_SCORE_UTIL_H diff --git a/src/libpentobi_base/Setup.h b/src/libpentobi_base/Setup.h new file mode 100644 index 0000000..2a74c64 --- /dev/null +++ b/src/libpentobi_base/Setup.h @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Setup.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_SETUP_H +#define LIBPENTOBI_BASE_SETUP_H + +#include "ColorMap.h" +#include "Move.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Definition of a setup position. + A setup position consists of a number of pieces that are placed at once + (in no particular order) on the board and a color to play next. */ +struct Setup +{ + /** Maximum number of pieces on board per color. */ + static const unsigned max_pieces = 24; + + typedef ArrayList PlacementList; + + Color to_play = Color(0); + + ColorMap placements; + + void clear(); +}; + +inline void Setup::clear() +{ + to_play = Color(0); + for_each_color([&](Color c) { placements[c].clear(); }); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_SETUP_H diff --git a/src/libpentobi_base/StartingPoints.cpp b/src/libpentobi_base/StartingPoints.cpp new file mode 100644 index 0000000..6c61f01 --- /dev/null +++ b/src/libpentobi_base/StartingPoints.cpp @@ -0,0 +1,99 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/StartingPoints.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "StartingPoints.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +void StartingPoints::add_colored_starting_point(const Geometry& geo, + unsigned x, unsigned y, + Color c) +{ + Point p = geo.get_point(x, y); + m_is_colored_starting_point[p] = true; + m_starting_point_color[p] = c; + m_starting_points[c].push_back(p); +} + +void StartingPoints::add_colorless_starting_point(const Geometry& geo, + unsigned x, unsigned y) +{ + Point p = geo.get_point(x, y); + m_is_colorless_starting_point[p] = true; + for_each_color([&](Color c) { + m_starting_points[c].push_back(p); + }); +} + +void StartingPoints::init(Variant variant, const Geometry& geo) +{ + m_is_colored_starting_point.fill(false, geo); + m_is_colorless_starting_point.fill(false, geo); + for_each_color([&](Color c) { + m_starting_points[c].clear(); + }); + switch (get_board_type(variant)) + { + case BoardType::classic: + add_colored_starting_point(geo, 0, 0, Color(0)); + add_colored_starting_point(geo, 19, 0, Color(1)); + add_colored_starting_point(geo, 19, 19, Color(2)); + add_colored_starting_point(geo, 0, 19, Color(3)); + break; + case BoardType::duo: + add_colored_starting_point(geo, 4, 4, Color(0)); + add_colored_starting_point(geo, 9, 9, Color(1)); + break; + case BoardType::trigon: + add_colorless_starting_point(geo, 17, 3); + add_colorless_starting_point(geo, 17, 14); + add_colorless_starting_point(geo, 9, 6); + add_colorless_starting_point(geo, 9, 11); + add_colorless_starting_point(geo, 25, 6); + add_colorless_starting_point(geo, 25, 11); + break; + case BoardType::trigon_3: + add_colorless_starting_point(geo, 15, 2); + add_colorless_starting_point(geo, 15, 13); + add_colorless_starting_point(geo, 7, 5); + add_colorless_starting_point(geo, 7, 10); + add_colorless_starting_point(geo, 23, 5); + add_colorless_starting_point(geo, 23, 10); + break; + case BoardType::nexos: + add_colored_starting_point(geo, 4, 3, Color(0)); + add_colored_starting_point(geo, 3, 4, Color(0)); + add_colored_starting_point(geo, 5, 4, Color(0)); + add_colored_starting_point(geo, 4, 5, Color(0)); + add_colored_starting_point(geo, 20, 3, Color(1)); + add_colored_starting_point(geo, 19, 4, Color(1)); + add_colored_starting_point(geo, 21, 4, Color(1)); + add_colored_starting_point(geo, 20, 5, Color(1)); + add_colored_starting_point(geo, 20, 19, Color(2)); + add_colored_starting_point(geo, 19, 20, Color(2)); + add_colored_starting_point(geo, 21, 20, Color(2)); + add_colored_starting_point(geo, 20, 21, Color(2)); + add_colored_starting_point(geo, 4, 19, Color(3)); + add_colored_starting_point(geo, 3, 20, Color(3)); + add_colored_starting_point(geo, 5, 20, Color(3)); + add_colored_starting_point(geo, 4, 21, Color(3)); + break; + case BoardType::callisto: + case BoardType::callisto_2: + case BoardType::callisto_3: + break; + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/StartingPoints.h b/src/libpentobi_base/StartingPoints.h new file mode 100644 index 0000000..acd748f --- /dev/null +++ b/src/libpentobi_base/StartingPoints.h @@ -0,0 +1,81 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/StartingPoints.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_STARTING_POINTS_H +#define LIBPENTOBI_BASE_STARTING_POINTS_H + +#include "Color.h" +#include "ColorMap.h" +#include "Geometry.h" +#include "Grid.h" +#include "Variant.h" +#include "libboardgame_util/ArrayList.h" + +namespace libpentobi_base { + +using libboardgame_util::ArrayList; + +//----------------------------------------------------------------------------- + +class StartingPoints +{ +public: + static const unsigned max_starting_points = 16; + + void init(Variant variant, const Geometry& geo); + + bool is_colored_starting_point(Point p) const; + + bool is_colorless_starting_point(Point p) const; + + Color get_starting_point_color(Point p) const; + + const ArrayList& + get_starting_points(Color c) const; + +private: + Grid m_is_colored_starting_point; + + Grid m_is_colorless_starting_point; + + Grid m_starting_point_color; + + ColorMap> m_starting_points; + + void add_colored_starting_point(const Geometry& geo, unsigned x, + unsigned y, Color c); + + void add_colorless_starting_point(const Geometry& geo, unsigned x, + unsigned y); +}; + +inline Color StartingPoints::get_starting_point_color(Point p) const +{ + LIBBOARDGAME_ASSERT(m_is_colored_starting_point[p]); + return m_starting_point_color[p]; +} + +inline const ArrayList& + StartingPoints::get_starting_points(Color c) const +{ + return m_starting_points[c]; +} + +inline bool StartingPoints::is_colored_starting_point(Point p) const +{ + return m_is_colored_starting_point[p]; +} + +inline bool StartingPoints::is_colorless_starting_point(Point p) const +{ + return m_is_colorless_starting_point[p]; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_STARTING_POINTS_H diff --git a/src/libpentobi_base/SymmetricPoints.cpp b/src/libpentobi_base/SymmetricPoints.cpp new file mode 100644 index 0000000..b392ad6 --- /dev/null +++ b/src/libpentobi_base/SymmetricPoints.cpp @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +/** @file SymmetricPoints.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SymmetricPoints.h" + +#include "libboardgame_base/PointTransform.h" + +namespace libpentobi_base { + +using libboardgame_base::PointTransfRot180; + +//----------------------------------------------------------------------------- + +void SymmetricPoints::init(const Geometry& geo) +{ + PointTransfRot180 transform; + for (Point p : geo) + m_symmetric_point[p] = transform.get_transformed(p, geo); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + diff --git a/src/libpentobi_base/SymmetricPoints.h b/src/libpentobi_base/SymmetricPoints.h new file mode 100644 index 0000000..151452a --- /dev/null +++ b/src/libpentobi_base/SymmetricPoints.h @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/SymmetricPoints.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_SYMMETRIC_POINTS_H +#define LIBPENTOBI_BASE_SYMMETRIC_POINTS_H + +#include "Geometry.h" +#include "Grid.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Lookup table to quickly get points that are symmetric with respect to the + center of the board. */ +class SymmetricPoints +{ +public: + void init(const Geometry& geo); + + Point operator[](Point p) const; + +private: + Grid m_symmetric_point; +}; + +inline Point SymmetricPoints::operator[](Point p) const +{ + return m_symmetric_point[p]; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_SYMMETRIC_POINTS_H diff --git a/src/libpentobi_base/TreeUtil.cpp b/src/libpentobi_base/TreeUtil.cpp new file mode 100644 index 0000000..8d38446 --- /dev/null +++ b/src/libpentobi_base/TreeUtil.cpp @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TreeUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TreeUtil.h" + +#include "NodeUtil.h" +#include "libboardgame_sgf/SgfUtil.h" + +namespace libpentobi_base { +namespace tree_util { + +using libboardgame_sgf::util::get_move_annotation; +using libboardgame_sgf::util::get_variation_string; + +//----------------------------------------------------------------------------- + +unsigned get_move_number(const PentobiTree& tree, const SgfNode& node) +{ + unsigned move_number = 0; + auto current = &node; + while (current) + { + if (! tree.get_move_ignore_invalid(*current).is_null()) + ++move_number; + if (libpentobi_base::node_util::has_setup(*current)) + break; + current = current->get_parent_or_null(); + } + return move_number; +} + +unsigned get_moves_left(const PentobiTree& tree, const SgfNode& node) +{ + unsigned moves_left = 0; + auto current = node.get_first_child_or_null(); + while (current) + { + if (libpentobi_base::node_util::has_setup(*current)) + break; + if (! tree.get_move_ignore_invalid(*current).is_null()) + ++moves_left; + current = current->get_first_child_or_null(); + } + return moves_left; +} + +string get_position_info(const PentobiTree& tree, const SgfNode& node) +{ + auto move = get_move_number(tree, node); + auto left = get_moves_left(tree, node); + auto total = move + left; + auto variation = get_variation_string(node); + auto annotation = get_move_annotation(tree, node); + ostringstream s; + if (left > 0 || move > 0) + s << move << annotation; + if (left > 0) + s << '/' << total; + if (! variation.empty()) + s << " (" << variation << ')'; + return s.str(); +} + +//----------------------------------------------------------------------------- + +} // namespace tree_util +} // namespace libpentobi_base diff --git a/src/libpentobi_base/TreeUtil.h b/src/libpentobi_base/TreeUtil.h new file mode 100644 index 0000000..34171d1 --- /dev/null +++ b/src/libpentobi_base/TreeUtil.h @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TreeUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_TREE_UTIL_H +#define LIBPENTOBI_BASE_TREE_UTIL_H + +#include "PentobiTree.h" + +namespace libpentobi_base { +namespace tree_util { + +//----------------------------------------------------------------------------- + +/** Get the move number at a node. + Counts the number of moves since the root node or the last node + that contained setup properties. Invalid moves are ignored. */ +unsigned get_move_number(const PentobiTree& tree, const SgfNode& node); + +/** Get the number of remaining moves in the current variation. + Counts the number of moves remaining in the current variation + until the end of the variation or the next node that contains setup + properties. Invalid moves are ignored. */ +unsigned get_moves_left(const PentobiTree& tree, const SgfNode& node); + +/** Return a single line that describes the location of the current move + in the tree. + Includes the move number, move annotationm symbols, the total number of + moves in this variation, and a string describing the variation. */ +string get_position_info(const PentobiTree& tree, const SgfNode& node); + +//----------------------------------------------------------------------------- + +} // namespace tree_util +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_TREE_UTIL_H diff --git a/src/libpentobi_base/TrigonGeometry.cpp b/src/libpentobi_base/TrigonGeometry.cpp new file mode 100644 index 0000000..706980a --- /dev/null +++ b/src/libpentobi_base/TrigonGeometry.cpp @@ -0,0 +1,128 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TrigonGeometry.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TrigonGeometry.h" + +namespace libpentobi_base { + +using libboardgame_base::CoordPoint; + +//----------------------------------------------------------------------------- + +map> TrigonGeometry::s_geometry; + +TrigonGeometry::TrigonGeometry(unsigned sz) +{ + m_sz = sz; + Geometry::init(sz * 4 - 1, sz * 2); +} + +const TrigonGeometry& TrigonGeometry::get(unsigned sz) +{ + auto pos = s_geometry.find(sz); + if (pos != s_geometry.end()) + return *pos->second; + shared_ptr geometry(new TrigonGeometry(sz)); + return *s_geometry.insert(make_pair(sz, geometry)).first->second; +} + +auto TrigonGeometry::get_adj_coord(int x, int y) const -> AdjCoordList +{ + AdjCoordList l; + if (get_point_type(x, y) == 0) + { + l.push_back(CoordPoint(x - 1, y)); + l.push_back(CoordPoint(x + 1, y)); + l.push_back(CoordPoint(x, y + 1)); + } + else + { + l.push_back(CoordPoint(x, y - 1)); + l.push_back(CoordPoint(x - 1, y)); + l.push_back(CoordPoint(x + 1, y)); + } + return l; +} + +auto TrigonGeometry::get_diag_coord(int x, int y) const -> DiagCoordList +{ + // The order does not matter logically but it is better to put far away + // points first because BoardConst uses the forbidden status of the first + // points during move generation and far away points can reject more moves. + DiagCoordList l; + if (get_point_type(x, y) == 0) + { + l.push_back(CoordPoint(x - 2, y)); + l.push_back(CoordPoint(x + 2, y)); + l.push_back(CoordPoint(x - 1, y - 1)); + l.push_back(CoordPoint(x + 1, y - 1)); + l.push_back(CoordPoint(x + 1, y + 1)); + l.push_back(CoordPoint(x - 1, y + 1)); + l.push_back(CoordPoint(x, y - 1)); + l.push_back(CoordPoint(x - 2, y + 1)); + l.push_back(CoordPoint(x + 2, y + 1)); + } + else + { + l.push_back(CoordPoint(x - 2, y)); + l.push_back(CoordPoint(x + 2, y)); + l.push_back(CoordPoint(x - 1, y + 1)); + l.push_back(CoordPoint(x + 1, y + 1)); + l.push_back(CoordPoint(x + 1, y - 1)); + l.push_back(CoordPoint(x - 1, y - 1)); + l.push_back(CoordPoint(x, y + 1)); + l.push_back(CoordPoint(x - 2, y - 1)); + l.push_back(CoordPoint(x + 2, y - 1)); + } + return l; +} + +unsigned TrigonGeometry::get_period_x() const +{ + return 2; +} + +unsigned TrigonGeometry::get_period_y() const +{ + return 2; +} + +unsigned TrigonGeometry::get_point_type(int x, int y) const +{ + if (m_sz % 2 == 0) + { + if (x % 2 == 0) + return y % 2 == 0 ? 1 : 0; + else + return y % 2 != 0 ? 1 : 0; + } + else + { + if (x % 2 != 0) + return y % 2 == 0 ? 1 : 0; + else + return y % 2 != 0 ? 1 : 0; + } +} + +bool TrigonGeometry::init_is_onboard(unsigned x, unsigned y) const +{ + auto width = get_width(); + auto height = get_height(); + unsigned dy = min(y, height - y - 1); + unsigned min_x = m_sz - dy - 1; + unsigned max_x = width - min_x - 1; + return x >= min_x && x <= max_x; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + diff --git a/src/libpentobi_base/TrigonGeometry.h b/src/libpentobi_base/TrigonGeometry.h new file mode 100644 index 0000000..03118ad --- /dev/null +++ b/src/libpentobi_base/TrigonGeometry.h @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TrigonGeometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_TRIGON_GEOMETRY_H +#define LIBPENTOBI_BASE_TRIGON_GEOMETRY_H + +#include +#include +#include "Geometry.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Geometry as used in the game Blokus Trigon. + The board is a hexagon consisting of triangles. The coordinates are like + in this example of a hexagon with edge size 3: + + 0 1 2 3 4 5 6 7 8 9 10 + 0 / \ / \ / \ / \ + 1 / \ / \ / \ / \ / \ + 2 / \ / \ / \ / \ / \ / \ + 3 \ / \ / \ / \ / \ / \ / + 4 \ / \ / \ / \ / \ / + 5 \ / \ / \ / \ / + + There are two point types: 0=upward triangle, 1=downward triangle. */ +class TrigonGeometry final + : public Geometry +{ +public: + /** Create or reuse an already created geometry with a given size. + @param sz The edge size of the hexagon. */ + static const TrigonGeometry& get(unsigned sz); + + + AdjCoordList get_adj_coord(int x, int y) const override; + + DiagCoordList get_diag_coord(int x, int y) const override; + + unsigned get_point_type(int x, int y) const override; + + unsigned get_period_x() const override; + + unsigned get_period_y() const override; + +protected: + bool init_is_onboard(unsigned x, unsigned y) const override; + +private: + /** Stores already created geometries by size. */ + static map> s_geometry; + + unsigned m_sz; + + + explicit TrigonGeometry(unsigned sz); +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_TRIGON_GEOMETRY_H diff --git a/src/libpentobi_base/TrigonTransform.cpp b/src/libpentobi_base/TrigonTransform.cpp new file mode 100644 index 0000000..39a0675 --- /dev/null +++ b/src/libpentobi_base/TrigonTransform.cpp @@ -0,0 +1,135 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TrigonTransform.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TrigonTransform.h" + +#include + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonIdentity::get_transformed(const CoordPoint& p) const +{ + return p; +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRefl::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.x, p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot60::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(ceil(0.5f * px - 1.5f * py)); + int y = static_cast(floor(0.5f * px + 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot120::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(ceil(-0.5f * px - 1.5f * py)); + int y = static_cast(ceil(0.5f * px - 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot180::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.x, -p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot240::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(floor(-0.5f * px + 1.5f * py)); + int y = static_cast(ceil(-0.5f * px - 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot300::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(floor(0.5f * px + 1.5f * py)); + int y = static_cast(floor(-0.5f * px + 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot60::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(ceil(0.5f * (-px) - 1.5f * py)); + int y = static_cast(floor(0.5f * (-px) + 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot120::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(ceil(-0.5f * (-px) - 1.5f * py)); + int y = static_cast(ceil(0.5f * (-px) - 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot180::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(p.x, -p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot240::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(floor(-0.5f * (-px) + 1.5f * py)); + int y = static_cast(ceil(-0.5f * (-px) - 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot300::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(floor(0.5f * (-px) + 1.5f * py)); + int y = static_cast(floor(-0.5f * (-px) + 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/TrigonTransform.h b/src/libpentobi_base/TrigonTransform.h new file mode 100644 index 0000000..4e33871 --- /dev/null +++ b/src/libpentobi_base/TrigonTransform.h @@ -0,0 +1,153 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TrigonTransform.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_TRIGON_TRANSFORM_H +#define LIBPENTOBI_BASE_TRIGON_TRANSFORM_H + +#include "libboardgame_base/Transform.h" + +namespace libpentobi_base { + +using libboardgame_base::CoordPoint; +using libboardgame_base::Transform; + +//----------------------------------------------------------------------------- + +class TransfTrigonIdentity + : public Transform +{ +public: + TransfTrigonIdentity() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRot60 + : public Transform +{ +public: + TransfTrigonRot60() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRot120 + : public Transform +{ +public: + TransfTrigonRot120() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRot180 + : public Transform +{ +public: + TransfTrigonRot180() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRot240 + : public Transform +{ +public: + TransfTrigonRot240() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRot300 + : public Transform +{ +public: + TransfTrigonRot300() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRefl + : public Transform +{ +public: + TransfTrigonRefl() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonReflRot60 + : public Transform +{ +public: + TransfTrigonReflRot60() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonReflRot120 + : public Transform +{ +public: + TransfTrigonReflRot120() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonReflRot180 + : public Transform +{ +public: + TransfTrigonReflRot180() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonReflRot240 + : public Transform +{ +public: + TransfTrigonReflRot240() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonReflRot300 + : public Transform +{ +public: + TransfTrigonReflRot300() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_TRIGON_TRANSFORM_H diff --git a/src/libpentobi_base/Variant.cpp b/src/libpentobi_base/Variant.cpp new file mode 100644 index 0000000..c0ee1f7 --- /dev/null +++ b/src/libpentobi_base/Variant.cpp @@ -0,0 +1,442 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Variant.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Variant.h" + +#include "CallistoGeometry.h" +#include "NexosGeometry.h" +#include "TrigonGeometry.h" +#include "libboardgame_base/RectGeometry.h" +#include "libboardgame_util/StringUtil.h" + +namespace libpentobi_base { + +using libboardgame_base::PointTransfIdent; +using libboardgame_base::PointTransfRefl; +using libboardgame_base::PointTransfReflRot180; +using libboardgame_base::PointTransfRot90; +using libboardgame_base::PointTransfRot180; +using libboardgame_base::PointTransfRot270; +using libboardgame_base::PointTransfRot90Refl; +using libboardgame_base::PointTransfRot270Refl; +using libboardgame_base::PointTransfTrigonReflRot60; +using libboardgame_base::PointTransfTrigonReflRot120; +using libboardgame_base::PointTransfTrigonReflRot240; +using libboardgame_base::PointTransfTrigonReflRot300; +using libboardgame_base::PointTransfTrigonRot60; +using libboardgame_base::PointTransfTrigonRot120; +using libboardgame_base::PointTransfTrigonRot240; +using libboardgame_base::PointTransfTrigonRot300; +using libboardgame_base::RectGeometry; +using libboardgame_util::trim; +using libboardgame_util::to_lower; + +//----------------------------------------------------------------------------- + +BoardType get_board_type(Variant variant) +{ + BoardType result = BoardType::classic; // Init to avoid compiler warning + switch (variant) + { + case Variant::duo: + case Variant::junior: + result = BoardType::duo; + break; + case Variant::classic: + case Variant::classic_2: + case Variant::classic_3: + result = BoardType::classic; + break; + case Variant::trigon: + case Variant::trigon_2: + result = BoardType::trigon; + break; + case Variant::trigon_3: + result = BoardType::trigon_3; + break; + case Variant::nexos: + case Variant::nexos_2: + result = BoardType::nexos; + break; + case Variant::callisto: + result = BoardType::callisto; + break; + case Variant::callisto_2: + result = BoardType::callisto_2; + break; + case Variant::callisto_3: + result = BoardType::callisto_3; + break; + } + return result; +} + +const Geometry& get_geometry(BoardType board_type) +{ + const Geometry* result = nullptr; // Init to avoid compiler warning + switch (board_type) + { + case BoardType::duo: + result = &RectGeometry::get(14, 14); + break; + case BoardType::classic: + result = &RectGeometry::get(20, 20); + break; + case BoardType::trigon: + result = &TrigonGeometry::get(9); + break; + case BoardType::trigon_3: + result = &TrigonGeometry::get(8); + break; + case BoardType::nexos: + result = &NexosGeometry::get(13); + break; + case BoardType::callisto: + result = &CallistoGeometry::get(4); + break; + case BoardType::callisto_2: + result = &CallistoGeometry::get(2); + break; + case BoardType::callisto_3: + result = &CallistoGeometry::get(3); + break; + } + return *result; +} + +const Geometry& get_geometry(Variant variant) +{ + return get_geometry(get_board_type(variant)); +} + +Color::IntType get_nu_colors(Variant variant) +{ + Color::IntType result = 0; // Init to avoid compiler warning + switch (variant) + { + case Variant::duo: + case Variant::junior: + case Variant::callisto_2: + result = 2; + break; + case Variant::trigon_3: + case Variant::callisto_3: + result = 3; + break; + case Variant::classic: + case Variant::classic_2: + case Variant::classic_3: + case Variant::trigon: + case Variant::trigon_2: + case Variant::nexos: + case Variant::nexos_2: + case Variant::callisto: + result = 4; + break; + } + return result; +} + +Color::IntType get_nu_players(Variant variant) +{ + Color::IntType result = 0; // Init to avoid compiler warning + switch (variant) + { + case Variant::duo: + case Variant::junior: + case Variant::classic_2: + case Variant::trigon_2: + case Variant::nexos_2: + case Variant::callisto_2: + result = 2; + break; + case Variant::classic_3: + case Variant::trigon_3: + case Variant::callisto_3: + result = 3; + break; + case Variant::classic: + case Variant::trigon: + case Variant::nexos: + case Variant::callisto: + result = 4; + break; + } + return result; +} + +PieceSet get_piece_set(Variant variant) +{ + PieceSet result = PieceSet::classic; // Init to avoid compiler warning + switch (variant) + { + case Variant::classic: + case Variant::classic_2: + case Variant::classic_3: + case Variant::duo: + result = PieceSet::classic; + break; + case Variant::trigon: + case Variant::trigon_2: + case Variant::trigon_3: + result = PieceSet::trigon; + break; + case Variant::junior: + result = PieceSet::junior; + break; + case Variant::nexos: + case Variant::nexos_2: + result = PieceSet::nexos; + break; + case Variant::callisto: + case Variant::callisto_2: + case Variant::callisto_3: + result = PieceSet::callisto; + break; + } + return result; +} + +void get_transforms(Variant variant, + vector>>& transforms, + vector>>& inv_transforms) +{ + transforms.clear(); + inv_transforms.clear(); + transforms.emplace_back(new PointTransfIdent); + inv_transforms.emplace_back(new PointTransfIdent); + switch (get_board_type(variant)) + { + case BoardType::duo: + transforms.emplace_back(new PointTransfRot270Refl); + inv_transforms.emplace_back(new PointTransfRot270Refl); + break; + case BoardType::trigon: + transforms.emplace_back(new PointTransfTrigonRot60); + inv_transforms.emplace_back(new PointTransfTrigonRot300); + transforms.emplace_back(new PointTransfTrigonRot120); + inv_transforms.emplace_back(new PointTransfTrigonRot240); + transforms.emplace_back(new PointTransfRot180); + inv_transforms.emplace_back(new PointTransfRot180); + transforms.emplace_back(new PointTransfTrigonRot240); + inv_transforms.emplace_back(new PointTransfTrigonRot120); + transforms.emplace_back(new PointTransfTrigonRot300); + inv_transforms.emplace_back(new PointTransfTrigonRot60); + transforms.emplace_back(new PointTransfRefl); + inv_transforms.emplace_back(new PointTransfRefl); + transforms.emplace_back(new PointTransfTrigonReflRot60); + inv_transforms.emplace_back(new PointTransfTrigonReflRot60); + transforms.emplace_back(new PointTransfTrigonReflRot120); + inv_transforms.emplace_back(new PointTransfTrigonReflRot120); + transforms.emplace_back(new PointTransfReflRot180); + inv_transforms.emplace_back(new PointTransfReflRot180); + transforms.emplace_back(new PointTransfTrigonReflRot240); + inv_transforms.emplace_back(new PointTransfTrigonReflRot240); + transforms.emplace_back(new PointTransfTrigonReflRot300); + inv_transforms.emplace_back(new PointTransfTrigonReflRot300); + break; + case BoardType::callisto_2: + case BoardType::callisto: + case BoardType::callisto_3: + transforms.emplace_back(new PointTransfRot90); + inv_transforms.emplace_back(new PointTransfRot270); + transforms.emplace_back(new PointTransfRot180); + inv_transforms.emplace_back(new PointTransfRot180); + transforms.emplace_back(new PointTransfRot270); + inv_transforms.emplace_back(new PointTransfRot90); + transforms.emplace_back(new PointTransfRefl); + inv_transforms.emplace_back(new PointTransfRefl); + transforms.emplace_back(new PointTransfReflRot180); + inv_transforms.emplace_back(new PointTransfReflRot180); + transforms.emplace_back(new PointTransfRot90Refl); + inv_transforms.emplace_back(new PointTransfRot90Refl); + transforms.emplace_back(new PointTransfRot270Refl); + inv_transforms.emplace_back(new PointTransfRot270Refl); + break; + case BoardType::classic: + case BoardType::trigon_3: + case BoardType::nexos: + break; + } +} + +bool has_central_symmetry(Variant variant) +{ + return variant == Variant::duo || variant == Variant::junior + || variant == Variant::trigon_2 || variant == Variant::callisto_2; +} + +bool parse_variant(const string& s, Variant& variant) +{ + string t = to_lower(trim(s)); + if (t == "blokus") + variant = Variant::classic; + else if (t == "blokus two-player") + variant = Variant::classic_2; + else if (t == "blokus three-player") + variant = Variant::classic_3; + else if (t == "blokus trigon") + variant = Variant::trigon; + else if (t == "blokus trigon two-player") + variant = Variant::trigon_2; + else if (t == "blokus trigon three-player") + variant = Variant::trigon_3; + else if (t == "blokus duo") + variant = Variant::duo; + else if (t == "blokus junior") + variant = Variant::junior; + else if (t == "nexos") + variant = Variant::nexos; + else if (t == "nexos two-player") + variant = Variant::nexos_2; + else if (t == "callisto") + variant = Variant::callisto; + else if (t == "callisto two-player") + variant = Variant::callisto_2; + else if (t == "callisto three-player") + variant = Variant::callisto_3; + else + return false; + return true; +} + +bool parse_variant_id(const string& s, Variant& variant) +{ + string t = to_lower(trim(s)); + if (t == "classic" || t == "c") + variant = Variant::classic; + else if (t == "classic_2" || t == "c2") + variant = Variant::classic_2; + else if (t == "classic_3" || t == "c3") + variant = Variant::classic_3; + else if (t == "trigon" || t == "t") + variant = Variant::trigon; + else if (t == "trigon_2" || t == "t2") + variant = Variant::trigon_2; + else if (t == "trigon_3" || t == "t3") + variant = Variant::trigon_3; + else if (t == "duo" || t == "d") + variant = Variant::duo; + else if (t == "junior" || t == "j") + variant = Variant::junior; + else if (t == "nexos" || t == "n") + variant = Variant::nexos; + else if (t == "nexos_2" || t == "n2") + variant = Variant::nexos_2; + else if (t == "callisto" || t == "ca") + variant = Variant::callisto; + else if (t == "callisto_2" || t == "ca2") + variant = Variant::callisto_2; + else if (t == "callisto_3" || t == "ca3") + variant = Variant::callisto_3; + else + return false; + return true; +} + +const char* to_string(Variant variant) +{ + const char* result = nullptr; // Init to avoid compiler warning + switch (variant) + { + case Variant::classic: + result = "Blokus"; + break; + case Variant::classic_2: + result = "Blokus Two-Player"; + break; + case Variant::classic_3: + result = "Blokus Three-Player"; + break; + case Variant::duo: + result = "Blokus Duo"; + break; + case Variant::junior: + result = "Blokus Junior"; + break; + case Variant::trigon: + result = "Blokus Trigon"; + break; + case Variant::trigon_2: + result = "Blokus Trigon Two-Player"; + break; + case Variant::trigon_3: + result = "Blokus Trigon Three-Player"; + break; + case Variant::nexos: + result = "Nexos"; + break; + case Variant::nexos_2: + result = "Nexos Two-Player"; + break; + case Variant::callisto: + result = "Callisto"; + break; + case Variant::callisto_2: + result = "Callisto Two-Player"; + break; + case Variant::callisto_3: + result = "Callisto Three-Player"; + break; + } + return result; +} + +const char* to_string_id(Variant variant) +{ + const char* result = nullptr; // Init to avoid compiler warning + switch (variant) + { + case Variant::classic: + result = "classic"; + break; + case Variant::classic_2: + result = "classic_2"; + break; + case Variant::classic_3: + result = "classic_3"; + break; + case Variant::duo: + result = "duo"; + break; + case Variant::junior: + result = "junior"; + break; + case Variant::trigon: + result = "trigon"; + break; + case Variant::trigon_2: + result = "trigon_2"; + break; + case Variant::trigon_3: + result = "trigon_3"; + break; + case Variant::nexos: + result = "nexos"; + break; + case Variant::nexos_2: + result = "nexos_2"; + break; + case Variant::callisto: + result = "callisto"; + break; + case Variant::callisto_2: + result = "callisto_2"; + break; + case Variant::callisto_3: + result = "callisto_3"; + break; + } + return result; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Variant.h b/src/libpentobi_base/Variant.h new file mode 100644 index 0000000..dcf8a94 --- /dev/null +++ b/src/libpentobi_base/Variant.h @@ -0,0 +1,150 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Variant.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_VARIANT_H +#define LIBPENTOBI_BASE_VARIANT_H + +#include +#include +#include +#include "Color.h" +#include "Geometry.h" +#include "libboardgame_base/PointTransform.h" + +namespace libpentobi_base { + +using libboardgame_base::PointTransform; + +//----------------------------------------------------------------------------- + +enum class PieceSet +{ + classic, + + junior, + + trigon, + + nexos, + + callisto +}; + +//----------------------------------------------------------------------------- + +enum class BoardType +{ + classic, + + duo, + + trigon, + + trigon_3, + + nexos, + + callisto, + + callisto_2, + + callisto_3, +}; + +//----------------------------------------------------------------------------- + +/** Game variant. */ +enum class Variant +{ + classic, + + classic_2, + + classic_3, + + duo, + + junior, + + trigon, + + trigon_2, + + trigon_3, + + nexos, + + nexos_2, + + callisto, + + callisto_2, + + callisto_3 +}; + +//----------------------------------------------------------------------------- + +/** Get name of game variant as in the GM property in Blokus SGF files. */ +const char* to_string(Variant variant); + +/** Get a short lowercase string without spaces that can be used as + a identifier for a game variant. + The strings used are "classic", "classic_2", "duo", "trigon", "trigon_2", + "trigon_3", "junior" */ +const char* to_string_id(Variant variant); + +/** Parse name of game variant as in the GM property in Blokus SGF files. + The parsing is case-insensitive, leading and trailing whitespaced are + ignored. + @param s + @param[out] variant + @result True if the string contained a valid game variant. */ +bool parse_variant(const string& s, Variant& variant); + +/** Parse short lowercase name of game variant as returned to_string_id(). + @param s + @param[out] variant + @result True if the string contained a valid game variant. */ +bool parse_variant_id(const string& s, Variant& variant); + +Color::IntType get_nu_colors(Variant variant); + +inline Color::Range get_colors(Variant variant) +{ + return Color::Range(get_nu_colors(variant)); +} + +Color::IntType get_nu_players(Variant variant); + +const Geometry& get_geometry(BoardType board_type); + +const Geometry& get_geometry(Variant variant); + +BoardType get_board_type(Variant variant); + +PieceSet get_piece_set(Variant variant); + +/** Get invariance transformations for a game variant. + The invariance transformations depend on the symmetry of the board type and + the starting points. + @param variant The game variant. + @param[out] transforms The invariance transformations. + @param[out] inv_transforms The inverse transformations of the elements in + transforms. */ +void get_transforms(Variant variant, + vector>>& transforms, + vector>>& inv_transforms); + +/** Is the variant a two-player variant with the board including the starting + points invariant through point reflection through its center? */ +bool has_central_symmetry(Variant variant); + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_VARIANT_H diff --git a/src/libpentobi_gui/BoardPainter.cpp b/src/libpentobi_gui/BoardPainter.cpp new file mode 100644 index 0000000..2c8c6de --- /dev/null +++ b/src/libpentobi_gui/BoardPainter.cpp @@ -0,0 +1,620 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/BoardPainter.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "BoardPainter.h" +#include "libpentobi_base/CallistoGeometry.h" + +#include +#include +#include "Util.h" + +using namespace std; +using libboardgame_util::ArrayList; +using libpentobi_base::BoardType; +using libpentobi_base::CallistoGeometry; +using libpentobi_base::Move; +using libpentobi_base::PieceSet; +using libpentobi_base::PointState; + +//----------------------------------------------------------------------------- + +BoardPainter::BoardPainter() +{ + m_font.setFamily("Helvetica"); + m_font.setStyleHint(QFont::SansSerif); + m_font.setStyleStrategy(QFont::PreferOutline); + m_fontSemiCondensed = m_font; + m_fontSemiCondensed.setStretch(QFont::SemiCondensed); + m_fontCondensed = m_font; + m_fontCondensed.setStretch(QFont::Condensed); + m_fontCoordLabels = m_font; + m_fontCoordLabels.setStretch(QFont::SemiCondensed); +} + +BoardPainter::~BoardPainter() = default; + +CoordPoint BoardPainter::getCoordPoint(int x, int y) +{ + if (! m_hasPainted) + return CoordPoint::null(); + x = static_cast((x - m_boardOffset.x()) / m_fieldWidth); + y = static_cast((y - m_boardOffset.y()) / m_fieldHeight); + if (x < 0 || x >= m_width || y < 0 || y >= m_height) + return CoordPoint::null(); + else + return CoordPoint(x, y); +} + +void BoardPainter::paintCoordinates(QPainter& painter) +{ + painter.setPen(m_coordinateColor); + for (int x = 0; x < m_width; ++x) + { + QString label; + if (x < 26) + label = QString(QChar('A' + x)); + else + { + label = "A"; + label.append(QChar('A' + (x - 26))); + } + paintLabel(painter, x * m_fieldWidth, m_height * m_fieldHeight, + m_fieldWidth, m_fieldHeight, label, true); + paintLabel(painter, x * m_fieldWidth, -m_fieldHeight, + m_fieldWidth, m_fieldHeight, label, true); + } + for (int y = 0; y < m_height; ++y) + { + QString label; + label.setNum(y + 1); + qreal left; + qreal right; + if (m_isTrigon) + { + left = -1.5 * m_fieldWidth; + right = (m_width + 0.5) * m_fieldWidth; + } + else + { + left = -m_fieldWidth; + right = m_width * m_fieldWidth; + } + paintLabel(painter, left, (m_height - y - 1) * m_fieldHeight, + m_fieldWidth, m_fieldHeight, label, true); + paintLabel(painter, right, (m_height - y - 1) * m_fieldHeight, + m_fieldWidth, m_fieldHeight, label, true); + } +} + +void BoardPainter::paintEmptyBoard(QPainter& painter, unsigned width, + unsigned height, Variant variant, + const Geometry& geo) +{ + m_hasPainted = true; + painter.setRenderHint(QPainter::Antialiasing, true); + m_variant = variant; + auto pieceSet = get_piece_set(variant); + m_geo = &geo; + m_width = static_cast(m_geo->get_width()); + m_height = static_cast(m_geo->get_height()); + m_isTrigon = (pieceSet == PieceSet::trigon); + m_isNexos = (pieceSet == PieceSet::nexos); + m_isCallisto = (pieceSet == PieceSet::callisto); + qreal ratio; + if (m_isTrigon) + { + ratio = 1.732; + if (m_coordinates) + m_fieldWidth = + min(qreal(width) / (m_width + 3), + height / (ratio * (m_height + 2))); + else + m_fieldWidth = + min(qreal(width) / (m_width + 1), height / (ratio * m_height)); + } + else + { + ratio = 1; + if (m_coordinates) + m_fieldWidth = + min(qreal(width) / (m_width + 2), + qreal(height) / (m_height + 2)); + else + m_fieldWidth = + min(qreal(width) / m_width, qreal(height) / m_height); + } + if (m_fieldWidth > 8) + // Prefer pixel alignment if board is not too small + m_fieldWidth = floor(m_fieldWidth); + m_fieldHeight = ratio * m_fieldWidth; + m_boardOffset = QPointF(0.5 * (width - m_fieldWidth * m_width), + 0.5 * (height - m_fieldHeight * m_height)); + // QFont::setPixelSize(0) prints a warning even if it works and the docs + // of Qt 5.3 don't forbid it (unlike QFont::setPointSize(0)). + int fontSize = + max(1, static_cast((m_isTrigon ? 0.7 : 0.5) * m_fieldWidth)); + m_font.setPixelSize(fontSize); + m_fontSemiCondensed.setPixelSize(fontSize); + m_fontCondensed.setPixelSize(fontSize); + m_fontCoordLabels.setPixelSize(fontSize); + painter.save(); + painter.translate(m_boardOffset); + if (m_coordinates) + paintCoordinates(painter); + if (m_isNexos) + painter.fillRect(QRectF(m_fieldWidth / 4, m_fieldHeight / 4, + m_width * m_fieldWidth - m_fieldWidth / 2, + m_height * m_fieldHeight - m_fieldHeight / 2), + QColor(174, 167, 172)); + auto nu_players = get_nu_players(m_variant); + for (Point p : *m_geo) + { + int x = m_geo->get_x(p); + int y = m_geo->get_y(p); + qreal fieldX = x * m_fieldWidth; + qreal fieldY = y * m_fieldHeight; + auto pointType = m_geo->get_point_type(p); + if (m_isTrigon) + { + bool isUpward = (pointType == 0); + Util::paintEmptyTriangle(painter, isUpward, fieldX, fieldY, + m_fieldWidth, m_fieldHeight); + } + else if (m_isNexos) + { + if (pointType == 1 || pointType == 2) + { + bool isHorizontal = (pointType == 1); + Util::paintEmptySegment(painter, isHorizontal, fieldX, fieldY, + m_fieldWidth); + } + else + { + LIBBOARDGAME_ASSERT(pointType == 0); + Util::paintEmptyJunction(painter, fieldX, fieldY, + m_fieldWidth); + } + } + else if (m_isCallisto + && CallistoGeometry::is_center_section(x, y, nu_players)) + Util::paintEmptySquareCallistoCenter(painter, fieldX, fieldY, + m_fieldWidth); + else if (m_isCallisto) + Util::paintEmptySquareCallisto(painter, fieldX, fieldY, + m_fieldWidth); + else + Util::paintEmptySquare(painter, fieldX, fieldY, m_fieldWidth); + } + painter.restore(); +} + +void BoardPainter::paintJunction(QPainter& painter, Variant variant, + const Grid& pointState, + const Grid& pieceId, int x, int y, + qreal fieldX, qreal fieldY) +{ + LIBBOARDGAME_ASSERT(m_geo->get_point_type(x, y) == 0); + ArrayList pieces; + if (x > 0) + { + auto piece = pieceId[m_geo->get_point(x - 1, y)]; + if (piece != 0) + pieces.include(piece); + } + if (x < m_width - 1) + { + auto piece = pieceId[m_geo->get_point(x + 1, y)]; + if (piece != 0) + pieces.include(piece); + } + if (y > 0) + { + auto piece = pieceId[m_geo->get_point(x, y - 1)]; + if (piece != 0) + pieces.include(piece); + } + if (y < m_height - 1) + { + auto piece = pieceId[m_geo->get_point(x, y + 1)]; + if (piece != 0) + pieces.include(piece); + } + for (auto piece : pieces) + { + Color c; + bool hasLeft = false; + if (x > 0) + { + Point p = m_geo->get_point(x - 1, y); + if (pieceId[p] == piece) + { + hasLeft = true; + c = pointState[p].to_color(); + } + } + bool hasRight = false; + if (x < m_width - 1) + { + Point p = m_geo->get_point(x + 1, y); + if (pieceId[p] == piece) + { + hasRight = true; + c = pointState[p].to_color(); + } + } + bool hasUp = false; + if (y > 0) + { + Point p = m_geo->get_point(x, y - 1); + if (pieceId[p] == piece) + { + hasUp = true; + c = pointState[p].to_color(); + } + } + bool hasDown = false; + if (y < m_height - 1) + { + Point p = m_geo->get_point(x, y + 1); + if (pieceId[p] == piece) + { + hasDown = true; + c = pointState[p].to_color(); + } + } + Util::paintJunction(painter, variant, c, fieldX, fieldY, m_fieldWidth, + m_fieldHeight, hasLeft, hasRight, hasUp, hasDown); + } +} + +void BoardPainter::paintLabel(QPainter& painter, qreal x, qreal y, + qreal width, qreal height, const QString& label, + bool isCoordLabel) +{ + if (isCoordLabel) + painter.setFont(m_fontCoordLabels); + else + painter.setFont(m_font); + QFontMetrics metrics(painter.font()); + QRect boundingRect = metrics.boundingRect(label); + if (! isCoordLabel) + { + if (boundingRect.width() > width) + { + painter.setFont(m_fontSemiCondensed); + QFontMetrics metrics(painter.font()); + boundingRect = metrics.boundingRect(label); + } + if (boundingRect.width() > width) + { + painter.setFont(m_fontCondensed); + QFontMetrics metrics(painter.font()); + boundingRect = metrics.boundingRect(label); + } + } + qreal dx = 0.5 * (width - boundingRect.width()); + qreal dy = 0.5 * (height - boundingRect.height()); + QRectF rect; + rect.setCoords(floor(x + dx), floor(y + dy), + ceil(x + width - dx + 1), ceil(y + height - dy + 1)); + painter.drawText(rect, Qt::TextDontClip, label); +} + +void BoardPainter::paintLabels(QPainter& painter, + const Grid& pointState, + Variant variant, const Grid& labels) +{ + for (Point p : *m_geo) + if (! labels[p].isEmpty()) + { + painter.setPen(Util::getLabelColor(variant, pointState[p])); + qreal x = m_geo->get_x(p) * m_fieldWidth; + qreal y = m_geo->get_y(p) * m_fieldHeight; + qreal width = m_fieldWidth; + qreal height = m_fieldHeight; + if (m_isTrigon) + { + bool isUpward = (m_geo->get_point_type(p) == 0); + if (isUpward) + y += 0.333 * height; + height = 0.666 * height; + } + paintLabel(painter, x, y, width, height, labels[p], false); + } +} + +void BoardPainter::paintMarks(QPainter& painter, + const Grid& pointState, + Variant variant, const Grid& marks) +{ + for (Point p : *m_geo) + if (marks[p] & (dot | circle)) + { + qreal x = (static_cast(m_geo->get_x(p)) + 0.5f) + * m_fieldWidth; + qreal y = (static_cast(m_geo->get_y(p)) + 0.5f) + * m_fieldHeight; + qreal size; + if (m_isTrigon) + { + bool isUpward = (m_geo->get_point_type(p) == 0); + if (isUpward) + y += 0.167 * m_fieldHeight; + else + y -= 0.167 * m_fieldHeight; + size = 0.1 * m_fieldHeight; + } + else if (m_isCallisto) + size = 0.1 * m_fieldHeight; + else + size = 0.12 * m_fieldHeight; + QColor color = Util::getMarkColor(variant, pointState[p]); + qreal penWidth = 0.05 * m_fieldHeight; + if (marks[p] & dot) + { + color.setAlphaF(0.5); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + size *= (1 + 0.25 * penWidth); + } + else + { + color.setAlphaF(0.6); + QPen pen(color); + pen.setWidthF(penWidth); + painter.setPen(pen); + painter.setBrush(Qt::NoBrush); + } + painter.drawEllipse(QPointF(x, y), size, size); + } +} + +void BoardPainter::paintPieces(QPainter& painter, + const Grid& pointState, + const Grid& pieceId, + const Grid* labels, + const Grid* marks) +{ + painter.setRenderHint(QPainter::Antialiasing, true); + painter.save(); + painter.translate(m_boardOffset); + ColorMap isFirstPiece(true); + for (Point p : *m_geo) + { + int x = m_geo->get_x(p); + int y = m_geo->get_y(p); + PointState s = pointState[p]; + qreal fieldX = x * m_fieldWidth; + qreal fieldY = y * m_fieldHeight; + auto pointType = m_geo->get_point_type(p); + if (m_isTrigon) + { + if (s.is_empty()) + continue; + Color c = s.to_color(); + isFirstPiece[c] = false; + bool isUpward = (pointType == 0); + Util::paintColorTriangle(painter, m_variant, c, isUpward, fieldX, + fieldY, m_fieldWidth, m_fieldHeight); + } + else if (m_isNexos) + { + if (pointType == 1 || pointType == 2) + { + if (s.is_empty()) + continue; + Color c = s.to_color(); + isFirstPiece[c] = false; + bool isHorizontal = (pointType == 1); + Util::paintColorSegment(painter, m_variant, c, isHorizontal, + fieldX, fieldY, m_fieldWidth); + } + else + { + LIBBOARDGAME_ASSERT(pointType == 0); + paintJunction(painter, m_variant, pointState, pieceId, x, y, + fieldX, fieldY); + } + } + else + { + if (s.is_empty()) + continue; + Color c = s.to_color(); + isFirstPiece[c] = false; + if (m_isCallisto) + { + bool hasLeft = + (x > 0 && m_geo->is_onboard(x - 1, y) + && pieceId[p] == pieceId[m_geo->get_point(x - 1, y)]); + bool hasRight = + (x < m_width - 1 && m_geo->is_onboard(x + 1, y) + && pieceId[p] == pieceId[m_geo->get_point(x + 1, y)]); + bool hasUp = + (y > 0 && m_geo->is_onboard(x, y - 1) + && pieceId[p] == pieceId[m_geo->get_point(x, y - 1)]); + bool hasDown = + (y < m_height - 1 && m_geo->is_onboard(x, y + 1) + && pieceId[p] == pieceId[m_geo->get_point(x, y + 1)]); + bool isOnePiece = + (! hasLeft && ! hasRight && ! hasUp && ! hasDown); + Util::paintColorSquareCallisto(painter, m_variant, c, fieldX, + fieldY, m_fieldWidth, hasRight, + hasDown, isOnePiece); + } + else + Util::paintColorSquare(painter, m_variant, c, fieldX, fieldY, + m_fieldWidth); + } + } + paintStartingPoints(painter, m_variant, pointState, isFirstPiece); + if (marks) + paintMarks(painter, pointState, m_variant, *marks); + if (labels) + paintLabels(painter, pointState, m_variant, *labels); + painter.restore(); +} + +void BoardPainter::paintSelectedPiece(QPainter& painter, Color c, + const MovePoints& points, bool isLegal) +{ + painter.setRenderHint(QPainter::Antialiasing, true); + painter.save(); + painter.translate(m_boardOffset); + qreal alpha; + qreal saturation; + bool flat; + if (isLegal) + { + alpha = 0.9; + saturation = 0.8; + flat = false; + } + else + { + alpha = 0.63; + saturation = 0.5; + flat = true; + } + ArrayList junctions; + for (Point p : points) + { + if (p.is_null()) + continue; + auto x = m_geo->get_x(p); + auto y = m_geo->get_y(p); + auto pointType = m_geo->get_point_type(p); + qreal fieldX = x * m_fieldWidth; + qreal fieldY = y * m_fieldHeight; + if (m_isTrigon) + { + bool isUpward = (pointType == 0); + Util::paintColorTriangle(painter, m_variant, c, isUpward, + fieldX, fieldY, m_fieldWidth, + m_fieldHeight, alpha, saturation, flat); + } + else if (m_isNexos) + { + if (pointType == 1 || pointType == 2) + { + bool isHorizontal = (pointType == 1); + Util::paintColorSegment(painter, m_variant, c, isHorizontal, + fieldX, fieldY, m_fieldWidth, alpha, + saturation, flat); + if (isHorizontal) + { + if (m_geo->is_onboard(x - 1, y)) + junctions.include(m_geo->get_point(x - 1, y)); + if (m_geo->is_onboard(x + 1, y)) + junctions.include(m_geo->get_point(x + 1, y)); + } + else + { + if (m_geo->is_onboard(x, y - 1)) + junctions.include(m_geo->get_point(x, y - 1)); + if (m_geo->is_onboard(x, y + 1)) + junctions.include(m_geo->get_point(x, y + 1)); + } + } + } + else if (m_isCallisto) + { + bool hasRight = (m_geo->is_onboard(CoordPoint(x + 1, y)) + && points.contains(m_geo->get_point(x + 1, y))); + bool hasDown = (m_geo->is_onboard(CoordPoint(x, y + 1)) + && points.contains(m_geo->get_point(x, y + 1))); + bool isOnePiece = (points.size() == 1); + Util::paintColorSquareCallisto(painter, m_variant, c, fieldX, + fieldY, m_fieldWidth, hasRight, + hasDown, isOnePiece, alpha, + saturation, flat); + } + else + Util::paintColorSquare(painter, m_variant, c, fieldX, fieldY, + m_fieldWidth, alpha, saturation, flat); + } + if (m_isNexos) + for (auto p : junctions) + { + auto x = m_geo->get_x(p); + auto y = m_geo->get_y(p); + bool hasLeft = (m_geo->is_onboard(CoordPoint(x - 1, y)) + && points.contains(m_geo->get_point(x - 1, y))); + bool hasRight = (m_geo->is_onboard(CoordPoint(x + 1, y)) + && points.contains(m_geo->get_point(x + 1, y))); + bool hasUp = (m_geo->is_onboard(CoordPoint(x, y - 1)) + && points.contains(m_geo->get_point(x, y - 1))); + bool hasDown = (m_geo->is_onboard(CoordPoint(x, y + 1)) + && points.contains(m_geo->get_point(x, y + 1))); + Util::paintJunction(painter, m_variant, c, x * m_fieldWidth, + y * m_fieldHeight, m_fieldWidth, m_fieldHeight, + hasLeft, hasRight, hasUp, hasDown, alpha, + saturation); + } + painter.restore(); +} + +void BoardPainter::paintStartingPoints(QPainter& painter, Variant variant, + const Grid& pointState, + const ColorMap& isFirstPiece) +{ + m_startingPoints.init(variant, *m_geo); + auto colors = get_colors(variant); + if (m_isTrigon) + { + bool isFirstPieceAny = false; + for (Color c : colors) + if (isFirstPiece[c]) + { + isFirstPieceAny = true; + break; + } + if (! isFirstPieceAny) + return; + for (Point p : m_startingPoints.get_starting_points(Color(0))) + { + if (! pointState[p].is_empty()) + continue; + int x = m_geo->get_x(p); + int y = m_geo->get_y(p); + qreal fieldX = x * m_fieldWidth; + qreal fieldY = y * m_fieldHeight; + bool isUpward = (m_geo->get_point_type(p) == 0); + Util::paintTriangleStartingPoint(painter, isUpward, fieldX, fieldY, + m_fieldWidth, m_fieldHeight); + } + } + else + { + for (Color c : colors) + { + if (! isFirstPiece[c]) + continue; + for (Point p : m_startingPoints.get_starting_points(c)) + { + if (! pointState[p].is_empty()) + continue; + int x = m_geo->get_x(p); + int y = m_geo->get_y(p); + qreal fieldX = x * m_fieldWidth; + qreal fieldY = y * m_fieldHeight; + if (m_isNexos) + Util::paintSegmentStartingPoint(painter, variant, c, + fieldX, fieldY, + m_fieldWidth); + else + Util::paintSquareStartingPoint(painter, variant, c, fieldX, + fieldY, m_fieldWidth); + } + } + } +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/BoardPainter.h b/src/libpentobi_gui/BoardPainter.h new file mode 100644 index 0000000..560fa67 --- /dev/null +++ b/src/libpentobi_gui/BoardPainter.h @@ -0,0 +1,147 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/BoardPainter.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_BOARD_PAINTER_H +#define LIBPENTOBI_GUI_BOARD_PAINTER_H + +#include +#include "libpentobi_base/Grid.h" +#include "libpentobi_base/Board.h" + +using libboardgame_base::CoordPoint; +using libboardgame_base::Transform; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::ColorMap; +using libpentobi_base::Variant; +using libpentobi_base::Geometry; +using libpentobi_base::Grid; +using libpentobi_base::MovePoints; +using libpentobi_base::PieceInfo; +using libpentobi_base::Point; +using libpentobi_base::PointState; +using libpentobi_base::StartingPoints; + +//----------------------------------------------------------------------------- + +/** Paints a board. + The painter can be used without having to create an instance of class Board, + which is undesirable for use cases like the thumbnailer because of the slow + creation of the BoardConst class. Instead, the board state is passed to the + paint() function as a grid of point states. */ +class BoardPainter +{ +public: + enum + { + dot = 1 << 1, + + circle = 1 << 2 + }; + + BoardPainter(); + + ~BoardPainter(); + + void setCoordinates(bool enable) { m_coordinates = enable; } + + void setCoordinateColor(const QColor& color) { m_coordinateColor = color; } + + /** Paint the board. + This function must be called before painting any pieces because it + initializes some members that are used by the piece painting + functions. */ + void paintEmptyBoard(QPainter& painter, unsigned width, unsigned height, + Variant variant, const Geometry& geo); + + /** Paint the pieces and markup. + The pieceId parameter only needs to be initialized in game variant + Nexos and is needed to paint the junctions between segment. Only + segment points of pieceId are used (point type 1 or 2) and must be 0 if + the point is empty or contain a unique value for segments of the same + piece. */ + void paintPieces(QPainter& painter, const Grid& pointState, + const Grid& pieceId, + const Grid* labels = nullptr, + const Grid* marks = nullptr); + + /** Paint the selected piece. + Paints the selected piece either transparent (if not legal) or opaque + (if legal). */ + void paintSelectedPiece(QPainter& painter, Color c, + const MovePoints& points, bool isLegal); + + /** Get the corresponding board coordinates of a pixel. + @return The board coordinates or CoordPoint::null() if paint() was + not called yet or the pixel is outside the board. */ + CoordPoint getCoordPoint(int x, int y); + + bool hasPainted() const { return m_hasPainted; } + +private: + bool m_hasPainted = false; + + bool m_coordinates = false; + + bool m_isTrigon; + + bool m_isNexos; + + bool m_isCallisto; + + const Geometry* m_geo; + + Variant m_variant; + + /** The width of the last board painted. */ + int m_width; + + /** The height of the last board painted. */ + int m_height; + + QColor m_coordinateColor = Qt::black; + + qreal m_fieldWidth; + + qreal m_fieldHeight; + + QPointF m_boardOffset; + + QFont m_font; + + QFont m_fontCondensed; + + QFont m_fontSemiCondensed; + + QFont m_fontCoordLabels; + + StartingPoints m_startingPoints; + + + void paintCoordinates(QPainter& painter); + + void paintJunction(QPainter& painter, Variant variant, + const Grid& pointState, + const Grid& pieceId, int x, int y, + qreal fieldX, qreal fieldY); + + void paintLabel(QPainter& painter, qreal x, qreal y, qreal width, + qreal height, const QString& label, bool isCoordLabel); + + void paintLabels(QPainter& painter, const Grid& pointState, + Variant variant, const Grid& labels); + + void paintMarks(QPainter& painter, const Grid& pointState, + Variant variant, const Grid& marks); + + void paintStartingPoints(QPainter& painter, Variant variant, + const Grid& pointState, + const ColorMap& isFirstPiece); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_BOARD_PAINTER_H diff --git a/src/libpentobi_gui/CMakeLists.txt b/src/libpentobi_gui/CMakeLists.txt new file mode 100644 index 0000000..9283f38 --- /dev/null +++ b/src/libpentobi_gui/CMakeLists.txt @@ -0,0 +1,87 @@ +set(CMAKE_AUTOMOC TRUE) + +set(pentobi_gui_STAT_SRCS + BoardPainter.h + BoardPainter.cpp + ComputerColorDialog.h + ComputerColorDialog.cpp + GameInfoDialog.h + GameInfoDialog.cpp + GuiBoard.h + GuiBoard.cpp + GuiBoardUtil.h + GuiBoardUtil.cpp + HelpWindow.h + HelpWindow.cpp + InitialRatingDialog.h + InitialRatingDialog.cpp + LeaveFullscreenButton.h + LeaveFullscreenButton.cpp + LineEdit.h + LineEdit.cpp + OrientationDisplay.h + OrientationDisplay.cpp + PieceSelector.h + PieceSelector.cpp + SameHeightLayout.h + SameHeightLayout.cpp + ScoreDisplay.h + ScoreDisplay.cpp + Util.h + Util.cpp +) + +set(pentobi_gui_ICNS + go-home.png + go-next.png + go-previous.png +) + +set(pentobi_gui_TS + translations/libpentobi_gui_de.ts + ) + +# Create PNG icons from SVG icons using the helper program src/convert +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/icons) +file(COPY libpentobi_gui_resources.qrc DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +foreach(icon ${pentobi_gui_ICNS}) + string(REPLACE ".png" ".svg" svgicon ${icon}) + add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${icon}" + COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ${CMAKE_CURRENT_BINARY_DIR}/icons/${icon} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ) +endforeach() +qt5_add_resources(pentobi_gui_RC_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/libpentobi_gui_resources.qrc + OPTIONS -no-compress) +file(COPY libpentobi_gui_resources_2x.qrc DESTINATION + ${CMAKE_CURRENT_BINARY_DIR}) +foreach(icon ${pentobi_gui_ICNS}) +string(REPLACE ".png" ".svg" svgicon ${icon}) +string(REPLACE ".png" "@2x.png" hdpiicon ${icon}) +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon}" + COMMAND convert --hdpi ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} +) +endforeach() +qt5_add_resources(pentobi_gui_RC_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/libpentobi_gui_resources_2x.qrc + OPTIONS -no-compress) + +qt5_add_translation(pentobi_gui_QM_SRCS ${pentobi_gui_TS}) + +add_library(pentobi_gui STATIC + ${pentobi_gui_STAT_SRCS} + ${pentobi_gui_RC_SRCS} + ${pentobi_gui_QM_SRCS}) + +target_link_libraries(pentobi_gui Qt5::Widgets) + +# Install translation files. If you change the destination, you need to +# update the default for PENTOBI_TRANSLATIONS in the main CMakeLists.txt +install(FILES ${pentobi_gui_QM_SRCS} + DESTINATION ${CMAKE_INSTALL_DATADIR}/pentobi/translations) diff --git a/src/libpentobi_gui/ComputerColorDialog.cpp b/src/libpentobi_gui/ComputerColorDialog.cpp new file mode 100644 index 0000000..ec354fb --- /dev/null +++ b/src/libpentobi_gui/ComputerColorDialog.cpp @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/ComputerColorDialog.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "ComputerColorDialog.h" + +#include +#include +#include + +//----------------------------------------------------------------------------- + +ComputerColorDialog::ComputerColorDialog(QWidget* parent, + Variant variant, + ColorMap& computerColor) + : QDialog(parent), + m_computerColor(computerColor), + m_variant(variant) +{ + setWindowTitle(tr("Computer Colors")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto layout = new QVBoxLayout; + setLayout(layout); + layout->setSizeConstraint(QLayout::SetFixedSize); + layout->addWidget(new QLabel(tr("Computer plays:"))); + for (Color::IntType i = 0; i < get_nu_players(m_variant); ++i) + createCheckBox(layout, Color(i)); + auto buttonBox = + new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttonBox); + connect(buttonBox, SIGNAL(accepted()), SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); + buttonBox->setFocus(); +} + +void ComputerColorDialog::accept() +{ + auto nuPlayers = get_nu_players(m_variant); + auto nuColors = get_nu_colors(m_variant); + if (nuPlayers == nuColors || m_variant == Variant::classic_3) + for (Color c : Color::Range(nuPlayers)) + m_computerColor[c] = m_checkBox[c.to_int()]->isChecked(); + else + { + LIBBOARDGAME_ASSERT(nuPlayers == 2 && nuColors == 4); + m_computerColor[Color(0)] = m_checkBox[0]->isChecked(); + m_computerColor[Color(2)] = m_checkBox[0]->isChecked(); + m_computerColor[Color(1)] = m_checkBox[1]->isChecked(); + m_computerColor[Color(3)] = m_checkBox[1]->isChecked(); + } + QDialog::accept(); +} + +void ComputerColorDialog::createCheckBox(QLayout* layout, Color c) +{ + auto checkBox = new QCheckBox(getPlayerString(c)); + checkBox->setChecked(m_computerColor[c]); + layout->addWidget(checkBox); + m_checkBox[c.to_int()] = checkBox; +} + +QString ComputerColorDialog::getPlayerString(Color c) +{ + auto nuPlayers = get_nu_players(m_variant); + auto nuColors = get_nu_colors(m_variant); + auto i = c.to_int(); + if (nuPlayers == 2 && nuColors == 4) + return i == 0 || i == 2 ? tr("&Blue/Red") : tr("&Yellow/Green"); + if (i == 0) + return tr("&Blue"); + if (i == 1) + return nuColors == 2 ? tr("&Green") : tr("&Yellow"); + if (i == 2) + return tr("&Red"); + LIBBOARDGAME_ASSERT(i == 3); + return tr("&Green"); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/ComputerColorDialog.h b/src/libpentobi_gui/ComputerColorDialog.h new file mode 100644 index 0000000..328a366 --- /dev/null +++ b/src/libpentobi_gui/ComputerColorDialog.h @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/ComputerColorDialog.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_COMPUTER_COLOR_DIALOG_H +#define LIBPENTOBI_GUI_COMPUTER_COLOR_DIALOG_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include "libpentobi_base/Variant.h" +#include "libpentobi_base/ColorMap.h" + +using namespace std; +using libpentobi_base::Variant; +using libpentobi_base::Color; +using libpentobi_base::ColorMap; + +//----------------------------------------------------------------------------- + +class ComputerColorDialog final + : public QDialog +{ + Q_OBJECT + +public: + ComputerColorDialog(QWidget* parent, Variant variant, + ColorMap& computerColor); + +public slots: + void accept() override; + +private: + ColorMap& m_computerColor; + + Variant m_variant; + + array m_checkBox; + + void createCheckBox(QLayout* layout, Color c); + + QString getPlayerString(Color c); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_COMPUTER_COLOR_DIALOG_H diff --git a/src/libpentobi_gui/GameInfoDialog.cpp b/src/libpentobi_gui/GameInfoDialog.cpp new file mode 100644 index 0000000..06bad4b --- /dev/null +++ b/src/libpentobi_gui/GameInfoDialog.cpp @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GameInfoDialog.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "GameInfoDialog.h" + +#include +#include "LineEdit.h" +#include "libpentobi_gui/Util.h" + +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +GameInfoDialog::GameInfoDialog(QWidget* parent, Game& game) + : QDialog(parent), + m_game(game), + m_charset(game.get_root().get_property("CA", "")) +{ + setWindowTitle(tr("Game Info")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto layout = new QVBoxLayout; + setLayout(layout); + m_formLayout = new QFormLayout; + layout->addLayout(m_formLayout); + auto variant = game.get_variant(); + auto nuColors = get_nu_colors(variant); + auto nuPlayers = get_nu_players(variant); + if (nuColors == 2) + { + m_playerBlue = createPlayerName(tr("Player &Blue:"), Color(0)); + m_playerGreen = createPlayerName(tr("Player &Green:"), Color(1)); + } + else if (nuPlayers == 2) + { + m_playerBlueRed = createPlayerName(tr("Player &Blue/Red:"), Color(0)); + m_playerYellowGreen = + createPlayerName(tr("Player &Yellow/Green:"), Color(1)); + } + else + { + m_playerBlue = createPlayerName(tr("Player &Blue:"), Color(0)); + m_playerYellow = createPlayerName(tr("Player &Yellow:"), Color(1)); + m_playerRed = createPlayerName(tr("Player &Red:"), Color(2)); + if (nuPlayers == 4) + m_playerGreen = createPlayerName(tr("Player &Green:"), Color(3)); + } + m_date = createLine(tr("&Date:"), m_game.get_date()); + m_time = createLine(tr("&Time limits:"), m_game.get_time()); + m_event = createLine(tr("&Event:"), m_game.get_event()); + m_round = createLine(tr("R&ound:"), m_game.get_round()); + auto buttonBox = + new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttonBox); + // We assume that the user wants to edit the game info if it is still empty + // and that he only wants to display it if not empty. Therefore, we leave + // the focus at the first text field if it is empty and put it on the + // button box otherwise. + if (nuColors == 4 && nuPlayers == 2) + { + if (! m_playerBlueRed->text().isEmpty()) + buttonBox->setFocus(); + } + else if (! m_playerBlue->text().isEmpty()) + buttonBox->setFocus(); + connect(buttonBox, SIGNAL(accepted()), SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); +} + +GameInfoDialog::~GameInfoDialog() +{ +} + +void GameInfoDialog::accept() +{ + auto variant = m_game.get_variant(); + auto nuColors = get_nu_colors(variant); + auto nuPlayers = get_nu_players(variant); + string value; + if (nuColors == 2) + { + if (acceptLine(m_playerBlue, value)) + m_game.set_player_name(Color(0), value); + if (acceptLine(m_playerGreen, value)) + m_game.set_player_name(Color(1), value); + } + else if (nuPlayers == 2) + { + if (acceptLine(m_playerBlueRed, value)) + m_game.set_player_name(Color(0), value); + if (acceptLine(m_playerYellowGreen, value)) + m_game.set_player_name(Color(1), value); + } + else + { + if (acceptLine(m_playerBlue, value)) + m_game.set_player_name(Color(0), value); + if (acceptLine(m_playerYellow, value)) + m_game.set_player_name(Color(1), value); + if (acceptLine(m_playerRed, value)) + m_game.set_player_name(Color(2), value); + if (nuPlayers == 4) + if (acceptLine(m_playerGreen, value)) + m_game.set_player_name(Color(3), value); + } + if (acceptLine(m_date, value)) + m_game.set_date(value); + if (acceptLine(m_time, value)) + m_game.set_time(value); + if (acceptLine(m_event, value)) + m_game.set_event(value); + if (acceptLine(m_round, value)) + m_game.set_round(value); + QDialog::accept(); +} + +bool GameInfoDialog::acceptLine(QLineEdit* lineEdit, string& value) +{ + if (! lineEdit->isModified()) + return false; + QString text = lineEdit->text(); + value = Util::convertSgfValueFromQString(text, m_charset); + return true; +} + +QLineEdit* GameInfoDialog::createLine(const QString& label, const string& text) +{ + auto lineEdit = new LineEdit(30); + if (! text.empty()) + { + lineEdit->setText(Util::convertSgfValueToQString(text, m_charset)); + lineEdit->setCursorPosition(0); + } + m_formLayout->addRow(label, lineEdit); + return lineEdit; +} + +QLineEdit* GameInfoDialog::createPlayerName(const QString& label, Color c) +{ + return createLine(label, m_game.get_player_name(c)); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/GameInfoDialog.h b/src/libpentobi_gui/GameInfoDialog.h new file mode 100644 index 0000000..f5a9c65 --- /dev/null +++ b/src/libpentobi_gui/GameInfoDialog.h @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GameInfoDialog.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_GAME_INFO_DIALOG_H +#define LIBPENTOBI_GUI_GAME_INFO_DIALOG_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include "libpentobi_base/Game.h" + +using namespace std; +using libpentobi_base::Color; +using libpentobi_base::Game; + +//----------------------------------------------------------------------------- + +class GameInfoDialog final + : public QDialog +{ + Q_OBJECT + +public: + GameInfoDialog(QWidget* parent, Game& game); + + ~GameInfoDialog(); + +public slots: + void accept() override; + +private: + Game& m_game; + + string m_charset; + + QFormLayout* m_formLayout; + + QLineEdit* m_playerBlue; + + QLineEdit* m_playerYellow; + + QLineEdit* m_playerRed; + + QLineEdit* m_playerGreen; + + QLineEdit* m_playerBlueRed; + + QLineEdit* m_playerYellowGreen; + + QLineEdit* m_date; + + QLineEdit* m_event; + + QLineEdit* m_round; + + QLineEdit* m_time; + + bool acceptLine(QLineEdit* lineEdit, string& value); + + QLineEdit* createLine(const QString& label, const string& text); + + QLineEdit* createPlayerName(const QString& label, Color c); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_GAME_INFO_DIALOG_H diff --git a/src/libpentobi_gui/GuiBoard.cpp b/src/libpentobi_gui/GuiBoard.cpp new file mode 100644 index 0000000..6c0e525 --- /dev/null +++ b/src/libpentobi_gui/GuiBoard.cpp @@ -0,0 +1,520 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GuiBoard.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "GuiBoard.h" + +#include +#include +#include "libboardgame_base/Transform.h" + +using namespace std; +using libboardgame_base::Transform; +using libpentobi_base::Geometry; +using libpentobi_base::MovePoints; +using libpentobi_base::PiecePoints; +using libpentobi_base::PieceSet; +using libpentobi_base::Point; +using libpentobi_base::PointState; + +//----------------------------------------------------------------------------- + +namespace { + +bool allPointEmpty(const Board& bd, Move mv) +{ + for (Point p : bd.get_move_points(mv)) + if (! bd.get_point_state(p).is_empty()) + return false; + return true; +} + +QPixmap* createPixmap(const QPainter& painter, const QSize& size) +{ + auto devicePixelRatio = painter.device()->devicePixelRatio(); + auto pixmap = new QPixmap(devicePixelRatio * size); + pixmap->setDevicePixelRatio(devicePixelRatio); + return pixmap; +} + +} // namespace + +//----------------------------------------------------------------------------- + +GuiBoard::GuiBoard(QWidget* parent, const Board& bd) + : QWidget(parent), + m_bd(bd) +{ + setMinimumSize(350, 350); + connect(&m_currentMoveShownAnimationTimer, SIGNAL(timeout()), + SLOT(showMoveAnimation())); +} + +void GuiBoard::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::StyleChange) + setEmptyBoardDirty(); +} + +void GuiBoard::clearMarkup() +{ + for (Point p : m_bd) + { + m_marks[p] = 0; + setLabel(p, ""); + } +} + +void GuiBoard::clearPiece() +{ + m_selectedPiece = Piece::null(); + m_selectedPieceTransform = nullptr; + setSelectedPiecePoints(); + setMouseTracking(false); +} + +void GuiBoard::copyFromBoard(const Board& bd) +{ + auto& geo = bd.get_geometry(); + auto variant = bd.get_variant(); + m_pointState.copy_from(bd.get_point_state(), geo); + auto pieceSet = get_piece_set(variant); + if (pieceSet == PieceSet::nexos || pieceSet == PieceSet::callisto) + { + m_pieceId.fill(0, geo); + unsigned n = 0; + for (Color c : bd.get_colors()) + for (Move mv : bd.get_setup().placements[c]) + { + ++n; + for (Point p : bd.get_move_points(mv)) + m_pieceId[p] = n; + } + for (auto mv : bd.get_moves()) + { + ++n; + for (Point p : bd.get_move_points(mv.move)) + m_pieceId[p] = n; + } + } + if (! m_isInitialized || m_variant != variant) + { + m_variant = variant; + m_isInitialized = true; + m_labels.fill("", geo); + m_marks.fill(0, geo); + setEmptyBoardDirty(); + } + else + setDirty(); +} + +Move GuiBoard::findSelectedPieceMove() +{ + if (m_selectedPiece.is_null() || m_selectedPieceOffset.is_null()) + return Move::null(); + const PiecePoints& points = + m_bd.get_piece_info(m_selectedPiece).get_points(); + auto& geo = m_bd.get_geometry(); + int width = static_cast(geo.get_width()); + int height = static_cast(geo.get_height()); + MovePoints movePoints; + for (CoordPoint p : points) + { + p = m_selectedPieceTransform->get_transformed(p); + int x = p.x + m_selectedPieceOffset.x; + int y = p.y + m_selectedPieceOffset.y; + if (x < 0 || x >= width || y < 0 || y >= height) + return Move::null(); + Point pp = geo.get_point(x, y); + if (pp.is_null()) + return Move::null(); + movePoints.push_back(pp); + } + Move mv; + if (! m_bd.find_move(movePoints, m_selectedPiece, mv) + || (m_freePlacement && ! allPointEmpty(m_bd, mv)) + || (! m_freePlacement + && ! m_bd.is_legal(m_selectedPieceColor, mv))) + return Move::null(); + else + return mv; +} + +void GuiBoard::leaveEvent(QEvent*) +{ + m_selectedPieceOffset = CoordPoint::null(); + setSelectedPiecePoints(); +} + +void GuiBoard::mouseMoveEvent(QMouseEvent* event) +{ + if (m_selectedPiece.is_null()) + return; + CoordPoint oldOffset = m_selectedPieceOffset; + setSelectedPieceOffset(*event); + if (m_selectedPieceOffset != oldOffset) + setSelectedPiecePoints(); +} + +void GuiBoard::mousePressEvent(QMouseEvent* event) +{ + if (m_selectedPiece.is_null()) + { + CoordPoint p = m_boardPainter.getCoordPoint(event->x(), event->y()); + auto& geo = m_bd.get_geometry(); + if (geo.is_onboard(p)) + emit pointClicked(geo.get_point(p.x, p.y)); + return; + } + setSelectedPieceOffset(*event); + placePiece(); +} + +void GuiBoard::movePieceDown() +{ + if (m_selectedPiece.is_null()) + return; + auto& geo = m_bd.get_geometry(); + CoordPoint newOffset; + if (m_selectedPieceOffset.is_null()) + { + newOffset = CoordPoint(geo.get_width() / 2, 0); + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + else + { + newOffset = m_selectedPieceOffset; + if (m_bd.get_piece_set() == PieceSet::trigon) + { + if (m_selectedPieceOffset.x % 2 == 0) + ++newOffset.x; + else + --newOffset.x; + ++newOffset.y; + } + else + newOffset.y += geo.get_period_y(); + if (geo.is_onboard(newOffset)) + { + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + } +} + +void GuiBoard::movePieceLeft() +{ + if (m_selectedPiece.is_null()) + return; + auto& geo = m_bd.get_geometry(); + CoordPoint newOffset; + if (m_selectedPieceOffset.is_null()) + { + newOffset = CoordPoint(geo.get_width() - 1, geo.get_height() / 2); + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + else + { + newOffset = m_selectedPieceOffset; + newOffset.x -= geo.get_period_x(); + if (geo.is_onboard(newOffset)) + { + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + } +} + +void GuiBoard::movePieceRight() +{ + if (m_selectedPiece.is_null()) + return; + auto& geo = m_bd.get_geometry(); + CoordPoint newOffset; + if (m_selectedPieceOffset.is_null()) + { + newOffset = CoordPoint(0, geo.get_height() / 2); + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + else + { + newOffset = m_selectedPieceOffset; + newOffset.x += geo.get_period_x(); + if (geo.is_onboard(newOffset)) + { + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + } +} + +void GuiBoard::movePieceUp() +{ + if (m_selectedPiece.is_null()) + return; + auto& geo = m_bd.get_geometry(); + CoordPoint newOffset; + if (m_selectedPieceOffset.is_null()) + { + newOffset = CoordPoint(geo.get_width() / 2, geo.get_height() - 1); + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + else + { + newOffset = m_selectedPieceOffset; + if (m_bd.get_piece_set() == PieceSet::trigon) + { + if (m_selectedPieceOffset.x % 2 == 0) + ++newOffset.x; + else + --newOffset.x; + --newOffset.y; + } + else + newOffset.y -= geo.get_period_y(); + if (geo.is_onboard(newOffset)) + { + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + } +} + +void GuiBoard::paintEvent(QPaintEvent*) +{ + if (! m_isInitialized) + return; + QPainter painter(this); + if (! m_emptyBoardPixmap || m_emptyBoardPixmap->size() != size()) + { + m_emptyBoardPixmap.reset(createPixmap(painter, size())); + m_emptyBoardDirty = true; + } + if (! m_boardPixmap || m_boardPixmap->size() != size()) + { + m_boardPixmap.reset(createPixmap(painter, size())); + m_dirty = true; + } + if (m_emptyBoardDirty) + { + QColor coordLabelColor = + QApplication::palette().color(QPalette::WindowText); + m_boardPainter.setCoordinateColor(coordLabelColor); + m_emptyBoardPixmap->fill(Qt::transparent); + QPainter painter(m_emptyBoardPixmap.get()); + m_boardPainter.paintEmptyBoard(painter, width(), height(), m_variant, + m_bd.get_geometry()); + m_emptyBoardDirty = false; + } + if (m_dirty) + { + m_boardPixmap->fill(Qt::transparent); + QPainter painter(m_boardPixmap.get()); + painter.drawPixmap(0, 0, *m_emptyBoardPixmap); + m_boardPainter.paintPieces(painter, m_pointState, m_pieceId, &m_labels, + &m_marks); + m_dirty = false; + } + painter.drawPixmap(0, 0, *m_boardPixmap); + if (m_isMoveShown) + { + if (m_currentMoveShownAnimationIndex % 2 == 0) + m_boardPainter.paintSelectedPiece(painter, m_currentMoveShownColor, + m_currentMoveShownPoints, true); + } + else if (! m_selectedPiecePoints.empty()) + { + bool isLegal = ! findSelectedPieceMove().is_null(); + m_boardPainter.paintSelectedPiece(painter, m_selectedPieceColor, + m_selectedPiecePoints, isLegal); + } +} + +void GuiBoard::placePiece() +{ + auto mv = findSelectedPieceMove(); + if (! mv.is_null()) + emit play(m_selectedPieceColor, mv); +} + +void GuiBoard::selectPiece(Color color, Piece piece) +{ + if (m_selectedPiece == piece && m_selectedPieceColor == color) + return; + m_selectedPieceColor = color; + m_selectedPieceTransform = m_bd.get_transforms().get_default(); + if (m_selectedPiece.is_null()) + m_selectedPieceOffset = CoordPoint::null(); + m_selectedPiece = piece; + setSelectedPieceOffset(m_selectedPieceOffset); + setSelectedPiecePoints(); + setMouseTracking(true); +} + +void GuiBoard::setEmptyBoardDirty() +{ + m_emptyBoardDirty = true; + m_dirty = true; + update(); +} + +void GuiBoard::setDirty() +{ + m_dirty = true; + update(); +} + +void GuiBoard::setCoordinates(bool enable) +{ + m_boardPainter.setCoordinates(enable); + setEmptyBoardDirty(); +} + +void GuiBoard::setFreePlacement(bool enable) +{ + m_freePlacement = enable; + update(); +} + +void GuiBoard::setLabel(Point p, const QString& text) +{ + if (! m_isInitialized) + return; + if (m_labels[p] != text) + { + m_labels[p] = text; + setDirty(); + } +} + +void GuiBoard::setMark(Point p, int mark, bool enable) +{ + if (! m_isInitialized) + return; + if (((m_marks[p] & mark) != 0) != enable) + { + m_marks[p] ^= mark; + setDirty(); + } +} + +void GuiBoard::setSelectedPieceOffset(const QMouseEvent& event) +{ + setSelectedPieceOffset(m_boardPainter.getCoordPoint(event.x(), event.y())); +} + +void GuiBoard::setSelectedPieceOffset(const CoordPoint& offset) +{ + if (offset.is_null()) + { + m_selectedPieceOffset = offset; + return; + } + auto& geo = m_bd.get_geometry(); + auto pieceSet = m_bd.get_piece_set(); + unsigned old_point_type = geo.get_point_type(offset); + CoordPoint type_matched_offset = offset; + if (pieceSet == PieceSet::trigon) + { + // Offset must match the point type (triangle up/down) of + // CoordPoint(0, 0) after the piece transformation + unsigned point_type = m_selectedPieceTransform->get_new_point_type(); + bool hasLeft = geo.is_onboard(CoordPoint(offset.x - 1, offset.y)); + bool hasRight = geo.is_onboard(CoordPoint(offset.x + 1, offset.y)); + if (old_point_type != point_type) + { + if ((point_type == 0 && hasRight) + || (point_type == 1 && ! hasLeft)) + ++type_matched_offset.x; + else + --type_matched_offset.x; + } + } + if (pieceSet == PieceSet::nexos) + { + // Offset must be a junction + if (old_point_type == 1) // horiz. segment + --type_matched_offset.x; + else if (old_point_type == 2) // vert. segment + --type_matched_offset.y; + else if (old_point_type == 3) // hole + { + --type_matched_offset.x; + --type_matched_offset.y; + } + } + m_selectedPieceOffset = type_matched_offset; +} + +void GuiBoard::setSelectedPiecePoints(Move mv) +{ + m_selectedPiecePoints.clear(); + for (Point p : m_bd.get_move_points(mv)) + m_selectedPiecePoints.push_back(p); + update(); +} + +void GuiBoard::setSelectedPiecePoints() +{ + m_selectedPiecePoints.clear(); + if (! m_selectedPiece.is_null() && ! m_selectedPieceOffset.is_null()) + { + auto& geo = m_bd.get_geometry(); + int width = static_cast(geo.get_width()); + int height = static_cast(geo.get_height()); + for (CoordPoint p : m_bd.get_piece_info(m_selectedPiece).get_points()) + { + p = m_selectedPieceTransform->get_transformed(p); + int x = p.x + m_selectedPieceOffset.x; + int y = p.y + m_selectedPieceOffset.y; + if (x >= 0 && x < width && y >= 0 && y < height) + m_selectedPiecePoints.push_back(geo.get_point(x, y)); + } + } + update(); +} + +void GuiBoard::setSelectedPieceTransform(const Transform* transform) +{ + if (m_selectedPieceTransform == transform) + return; + m_selectedPieceTransform = transform; + setSelectedPieceOffset(m_selectedPieceOffset); + setSelectedPiecePoints(); +} + +void GuiBoard::showMove(Color c, Move mv) +{ + m_isMoveShown = true; + m_currentMoveShownColor = c; + m_currentMoveShownPoints.clear(); + for (Point p : m_bd.get_move_points(mv)) + m_currentMoveShownPoints.push_back(p); + m_currentMoveShownAnimationIndex = 0; + m_currentMoveShownAnimationTimer.start(500); + update(); +} + +void GuiBoard::showMoveAnimation() +{ + ++m_currentMoveShownAnimationIndex; + if (m_currentMoveShownAnimationIndex > 5) + { + m_isMoveShown = false; + m_currentMoveShownAnimationTimer.stop(); + } + update(); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/GuiBoard.h b/src/libpentobi_gui/GuiBoard.h new file mode 100644 index 0000000..dc48ef3 --- /dev/null +++ b/src/libpentobi_gui/GuiBoard.h @@ -0,0 +1,188 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GuiBoard.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_GUI_BOARD_H +#define LIBPENTOBI_GUI_GUI_BOARD_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include "BoardPainter.h" +#include "libboardgame_base/CoordPoint.h" +#include "libpentobi_base/Board.h" + +using namespace std; +using libpentobi_base::Color; +using libboardgame_base::CoordPoint; +using libpentobi_base::Board; +using libpentobi_base::Grid; +using libpentobi_base::Move; +using libpentobi_base::Piece; +using libpentobi_base::PieceInfo; +using libpentobi_base::Point; + +//----------------------------------------------------------------------------- + +class GuiBoard + : public QWidget +{ + Q_OBJECT + +public: + GuiBoard(QWidget* parent, const Board& bd); + + void setCoordinates(bool enable); + + const Board& getBoard() const; + + const Grid& getLabels() const; + + Piece getSelectedPiece() const; + + const Transform* getSelectedPieceTransform() const; + + void setSelectedPieceTransform(const Transform* transform); + + void showMove(Color c, Move mv); + + void copyFromBoard(const Board& bd); + + void setLabel(Point p, const QString& text); + + void setMark(Point p, int mark, bool enable = true); + + void clearMarkup(); + + void setFreePlacement(bool enable); + + void setSelectedPiecePoints(Move mv); + +public slots: + void clearPiece(); + + void selectPiece(Color color, Piece piece); + + void movePieceLeft(); + + void movePieceRight(); + + void movePieceUp(); + + void movePieceDown(); + + void placePiece(); + +signals: + void play(Color color, Move mv); + + void pointClicked(Point p); + +protected: + void changeEvent(QEvent* event) override; + + void leaveEvent(QEvent* event) override; + + void mouseMoveEvent(QMouseEvent* event) override; + + void mousePressEvent(QMouseEvent* event) override; + + void paintEvent(QPaintEvent* event) override; + +private: + const Board& m_bd; + + bool m_isInitialized = false; + + bool m_freePlacement = false; + + /** Does the empty board need redrawing? */ + bool m_emptyBoardDirty = true; + + /** Do the pieces and markup on the board need redrawing? + If true, the cached board pixmap needs to be repainted. This does not + include the selected piece (the selected piece is always painted). */ + bool m_dirty = true; + + bool m_isMoveShown = false; + + Variant m_variant; + + Board::PointStateGrid m_pointState; + + Grid m_pieceId; + + Piece m_selectedPiece = Piece::null(); + + Color m_selectedPieceColor; + + const Transform* m_selectedPieceTransform = nullptr; + + CoordPoint m_selectedPieceOffset; + + MovePoints m_selectedPiecePoints; + + Grid m_labels; + + Grid m_marks; + + BoardPainter m_boardPainter; + + unique_ptr m_emptyBoardPixmap; + + unique_ptr m_boardPixmap; + + Color m_currentMoveShownColor; + + MovePoints m_currentMoveShownPoints; + + int m_currentMoveShownAnimationIndex; + + QTimer m_currentMoveShownAnimationTimer; + + Move findSelectedPieceMove(); + + void setEmptyBoardDirty(); + + void setDirty(); + + void setSelectedPieceOffset(const QMouseEvent& event); + + void setSelectedPieceOffset(const CoordPoint& offset); + + void setSelectedPiecePoints(); + +private slots: + void showMoveAnimation(); +}; + +inline const Board& GuiBoard::getBoard() const +{ + return m_bd; +} + +inline const Grid& GuiBoard::getLabels() const +{ + return m_labels; +} + +inline Piece GuiBoard::getSelectedPiece() const +{ + return m_selectedPiece; +} + +inline const Transform* GuiBoard::getSelectedPieceTransform() const +{ + return m_selectedPieceTransform; +} + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_GUI_BOARD_H diff --git a/src/libpentobi_gui/GuiBoardUtil.cpp b/src/libpentobi_gui/GuiBoardUtil.cpp new file mode 100644 index 0000000..d560eeb --- /dev/null +++ b/src/libpentobi_gui/GuiBoardUtil.cpp @@ -0,0 +1,115 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GuiBoardUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "GuiBoardUtil.h" + +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_util/StringUtil.h" + +namespace gui_board_util { + +using libpentobi_base::ColorMove; +using libpentobi_base::PentobiTree; +using libboardgame_sgf::SgfNode; +using libboardgame_sgf::util::is_main_variation; +using libboardgame_sgf::util::get_move_annotation; +using libboardgame_util::get_letter_coord; + +//----------------------------------------------------------------------------- + +namespace { + +/** Get the index of a variation. + This ignores child nodes without moves so that the moves are still labeled + 1a, 1b, 1c, etc. even if this does not correspond to the child node + index. (Note that this is a different convention from variation strings + which does not use move number and child move index, but node depth and + child node index) */ +bool getVariationIndex(const PentobiTree& tree, const SgfNode& node, + unsigned& moveIndex) +{ + auto parent = node.get_parent_or_null(); + if (! parent || parent->has_single_child()) + return false; + unsigned nuSiblingMoves = 0; + moveIndex = 0; + for (auto& i : parent->get_children()) + { + if (! tree.has_move(i)) + continue; + if (&i == &node) + moveIndex = nuSiblingMoves; + ++nuSiblingMoves; + } + if (nuSiblingMoves == 1) + return false; + return true; +} + +void markMove(GuiBoard& guiBoard, const Game& game, const SgfNode& node, + unsigned moveNumber, ColorMove mv, bool markVariations, + bool markWithDot) +{ + if (mv.is_null()) + return; + auto& bd = game.get_board(); + Point p = bd.get_move_info_ext_2(mv.move).label_pos; + if (markWithDot) + { + if (markVariations && ! is_main_variation(game.get_current())) + guiBoard.setMark(p, BoardPainter::circle); + else + guiBoard.setMark(p, BoardPainter::dot); + return; + } + QString label; + label.setNum(moveNumber); + if (markVariations) + { + unsigned moveIndex; + if (getVariationIndex(game.get_tree(), node, moveIndex)) + label.append(get_letter_coord(moveIndex).c_str()); + } + label.append(get_move_annotation(game.get_tree(), node)); + guiBoard.setLabel(p, label); +} + +} // namespace + +//----------------------------------------------------------------------------- + +void setMarkup(GuiBoard& guiBoard, const Game& game, unsigned markMovesBegin, + unsigned markMovesEnd, bool markVariations, bool markWithDot) +{ + guiBoard.clearMarkup(); + if (markMovesBegin == 0) + return; + auto& tree = game.get_tree(); + auto& bd = game.get_board(); + unsigned moveNumber = bd.get_nu_moves(); + auto node = &game.get_current(); + do + { + auto mv = tree.get_move_ignore_invalid(*node); + if (! mv.is_null()) + { + if (moveNumber >= markMovesBegin && moveNumber <= markMovesEnd) + markMove(guiBoard, game, *node, moveNumber, mv, markVariations, + markWithDot); + --moveNumber; + } + node = node->get_parent_or_null(); + } + while (node); +} + +//----------------------------------------------------------------------------- + +} // namespace gui_board_util diff --git a/src/libpentobi_gui/GuiBoardUtil.h b/src/libpentobi_gui/GuiBoardUtil.h new file mode 100644 index 0000000..951427b --- /dev/null +++ b/src/libpentobi_gui/GuiBoardUtil.h @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GuiBoardUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_GUI_BOARD_UTIL_H +#define LIBPENTOBI_GUI_GUI_BOARD_UTIL_H + +#include "GuiBoard.h" +#include "libpentobi_base/Game.h" + +namespace gui_board_util { + +using libpentobi_base::Game; + +//----------------------------------------------------------------------------- + +void setMarkup(GuiBoard& guiBoard, const Game& game, + unsigned markMovesBegin, unsigned markMovesEnd, + bool markVariations, bool markWithDot); + +//----------------------------------------------------------------------------- + +} // namespace gui_board_util + +#endif // LIBPENTOBI_GUI_GUI_BOARD_UTIL_H diff --git a/src/libpentobi_gui/HelpWindow.cpp b/src/libpentobi_gui/HelpWindow.cpp new file mode 100644 index 0000000..0848c33 --- /dev/null +++ b/src/libpentobi_gui/HelpWindow.cpp @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/HelpWindow.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "HelpWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "libboardgame_util/Log.h" + +//----------------------------------------------------------------------------- + +namespace { + +void setIcon(QAction* action, const QString& name) +{ + QString fallback = QString(":/libpentobi_gui/icons/%1.png").arg(name); + action->setIcon(QIcon::fromTheme(name, QIcon(fallback))); +} + +} // namespace + +//----------------------------------------------------------------------------- + +HelpWindow::HelpWindow(QWidget* parent, const QString& title, + const QString& mainPage) + : QMainWindow(parent) +{ + LIBBOARDGAME_LOG("Loading ", mainPage.toLocal8Bit().constData()); + setWindowTitle(title); + if (QIcon::hasThemeIcon("help-browser")) + setWindowIcon(QIcon::fromTheme("help-browser")); + m_mainPageUrl = QUrl::fromLocalFile(mainPage); + auto browser = new QTextBrowser; + setCentralWidget(browser); + browser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + browser->setSource(m_mainPageUrl); + auto actionBack = new QAction(tr("Back"), this); + actionBack->setToolTip(tr("Show previous page in history")); + actionBack->setEnabled(false); + setIcon(actionBack, "go-previous"); + connect(actionBack, SIGNAL(triggered()), browser, SLOT(backward())); + connect(browser, SIGNAL(backwardAvailable(bool)), + actionBack, SLOT(setEnabled(bool))); + auto actionForward = new QAction(tr("Forward"), this); + actionForward->setToolTip(tr("Show next page in history")); + actionForward->setEnabled(false); + setIcon(actionForward, "go-next"); + connect(actionForward, SIGNAL(triggered()), browser, SLOT(forward())); + connect(browser, SIGNAL(forwardAvailable(bool)), + actionForward, SLOT(setEnabled(bool))); + m_actionHome = new QAction(tr("Contents"), this); + m_actionHome->setToolTip(tr("Show table of contents")); + m_actionHome->setEnabled(false); + setIcon(m_actionHome, "go-home"); + connect(m_actionHome, SIGNAL(triggered()), browser, SLOT(home())); + connect(browser, SIGNAL(sourceChanged(const QUrl&)), + SLOT(handleSourceChanged(const QUrl&))); + auto actionClose = new QAction("", this); + actionClose->setShortcut(QKeySequence::Close); + connect(actionClose, SIGNAL(triggered()), SLOT(hide())); + addAction(actionClose); + auto toolBar = new QToolBar; + toolBar->setMovable(false); + toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); + toolBar->addAction(actionBack); + toolBar->addAction(actionForward); + toolBar->addAction(m_actionHome); + addToolBar(toolBar); + QSettings settings; + if (! restoreGeometry(settings.value("helpwindow_geometry").toByteArray())) + adjustSize(); +} + +QString HelpWindow::findMainPage(QString helpDir, QString appName) +{ + auto locale = QLocale::system().name(); + auto path = QString("%1/%2/%3/index.html").arg(helpDir, locale, appName); + if (QFile(path).exists()) + return path; + path = QString("%1/%2/%3/index.html") + .arg(helpDir, locale.split("_")[0], appName); + if (QFile(path).exists()) + return path; + return QString("%1/C/%3/index.html").arg(helpDir, appName); +} + +void HelpWindow::closeEvent(QCloseEvent* event) +{ + QSettings settings; + settings.setValue("helpwindow_geometry", saveGeometry()); + QMainWindow::closeEvent(event); +} + +void HelpWindow::handleSourceChanged(const QUrl& src) +{ + m_actionHome->setEnabled(src != m_mainPageUrl); +} + +QSize HelpWindow::sizeHint() const +{ + auto geo = QApplication::desktop()->screenGeometry(); + return QSize(geo.width() * 4 / 10, geo.height() * 9 / 10); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/HelpWindow.h b/src/libpentobi_gui/HelpWindow.h new file mode 100644 index 0000000..d26328c --- /dev/null +++ b/src/libpentobi_gui/HelpWindow.h @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/HelpWindow.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_HELP_WINDOW_H +#define LIBPENTOBI_GUI_HELP_WINDOW_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +//----------------------------------------------------------------------------- + +class HelpWindow + : public QMainWindow +{ + Q_OBJECT + +public: + /** Find the main page for a given language. + Assumes that the layout of the help directory is according to + http://www.freedesktop.org/wiki/Specifications/help-spec/ + @param helpDir The help directory. + @param appName The subdirectory name for the application. + @return The full path of index.html. */ + static QString findMainPage(QString helpDir, QString appName); + + HelpWindow(QWidget* parent, const QString& title, const QString& mainPage); + + QSize sizeHint() const override; + +protected: + void closeEvent(QCloseEvent* event) override; + +private: + QUrl m_mainPageUrl; + + QAction* m_actionHome; + +private slots: + void handleSourceChanged(const QUrl& src); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_HELP_WINDOW_H diff --git a/src/libpentobi_gui/InitialRatingDialog.cpp b/src/libpentobi_gui/InitialRatingDialog.cpp new file mode 100644 index 0000000..80144a8 --- /dev/null +++ b/src/libpentobi_gui/InitialRatingDialog.cpp @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/InitialRatingDialog.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "InitialRatingDialog.h" + +#include +#include +#include +#include + +//----------------------------------------------------------------------------- + +InitialRatingDialog::InitialRatingDialog(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(tr("Initial Rating")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto layout = new QVBoxLayout; + setLayout(layout); + layout->setSizeConstraint(QLayout::SetFixedSize); + auto label = + new QLabel(tr("You have not yet played rated games in this game" + " variant. Estimate your playing strength to" + " initialize your rating.")); + label->setWordWrap(true); + layout->addWidget(label); + auto sliderBoxLayout = new QHBoxLayout; + layout->addLayout(sliderBoxLayout); + sliderBoxLayout->addWidget(new QLabel(tr("Beginner"))); + m_slider = new QSlider(Qt::Horizontal); + m_slider->setMinimum(1000); + m_slider->setMaximum(2000); + m_slider->setSingleStep(10); + m_slider->setPageStep(100); + sliderBoxLayout->addWidget(m_slider); + sliderBoxLayout->addWidget(new QLabel(tr("Expert"))); + m_ratingLabel = new QLabel; + layout->addWidget(m_ratingLabel); + setRating(1000); + connect(m_slider, SIGNAL(valueChanged(int)), SLOT(setRating(int))); + auto buttonBox = + new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttonBox); + connect(buttonBox, SIGNAL(accepted()), SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); +} + +void InitialRatingDialog::setRating(int rating) +{ + m_rating = rating; + m_ratingLabel->setText(tr("Your initial rating: %1").arg(rating)); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/InitialRatingDialog.h b/src/libpentobi_gui/InitialRatingDialog.h new file mode 100644 index 0000000..651370c --- /dev/null +++ b/src/libpentobi_gui/InitialRatingDialog.h @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/InitialRatingDialog.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_INITIAL_RATING_DIALOG_H +#define LIBPENTOBI_GUI_INITIAL_RATING_DIALOG_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +class QLabel; +class QSlider; + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Dialog that asks the user to estimate his initial rating. */ +class InitialRatingDialog final + : public QDialog +{ + Q_OBJECT + +public: + explicit InitialRatingDialog(QWidget* parent); + + int getRating() const; + +public slots: + void setRating(int rating); + +private: + int m_rating; + + QSlider* m_slider; + + QLabel* m_ratingLabel; +}; + +inline int InitialRatingDialog::getRating() const +{ + return m_rating; +} + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_INITIAL_RATING_DIALOG_H diff --git a/src/libpentobi_gui/LeaveFullscreenButton.cpp b/src/libpentobi_gui/LeaveFullscreenButton.cpp new file mode 100644 index 0000000..2f96636 --- /dev/null +++ b/src/libpentobi_gui/LeaveFullscreenButton.cpp @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/LeaveFullscreenButton.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "LeaveFullscreenButton.h" + +#include +#include +#include +#include +#include + +//----------------------------------------------------------------------------- + +LeaveFullscreenButton::LeaveFullscreenButton(QWidget* parent, QAction* action) + : QObject(parent) +{ + m_timer = new QTimer; + m_timer->setSingleShot(true); + m_triggerArea = new QWidget(parent); + m_triggerArea->setMouseTracking(true); + m_button = new QToolButton(parent); + m_button->setDefaultAction(action); + m_button->setToolTip(""); + m_button->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_button->show(); + // Resize to size hint as a workaround for a bug that clips the + // long button text (tested on Qt 4.8.3 on Linux/KDE). + m_button->resize(m_button->sizeHint()); + int x = qApp->desktop()->screenGeometry().width() - m_button->width(); + m_buttonPos = QPoint(x, 0); + m_triggerArea->resize(m_button->width(), m_button->height() / 2); + m_triggerArea->move(m_buttonPos); + m_animation = new QPropertyAnimation(m_button, "pos"); + m_animation->setDuration(1000); + m_animation->setStartValue(m_buttonPos); + m_animation->setEndValue(QPoint(x, -m_button->height() + 5)); + qApp->installEventFilter(this); + connect(m_timer, SIGNAL(timeout()), SLOT(slideOut())); +} + +void LeaveFullscreenButton::hideButton() +{ + m_animation->stop(); + m_timer->stop(); + m_triggerArea->hide(); + m_button->hide(); +} + +bool LeaveFullscreenButton::eventFilter(QObject* watched, QEvent* event) +{ + if (m_button->isVisible() && event->type() == QEvent::MouseMove + && (watched == m_triggerArea || watched == m_button)) + showButton(); + return false; +} + +void LeaveFullscreenButton::showButton() +{ + m_animation->stop(); + m_button->move(m_buttonPos); + m_button->show(); + m_triggerArea->hide(); + m_timer->start(5000); +} + +void LeaveFullscreenButton::slideOut() +{ + m_triggerArea->show(); + m_animation->start(); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/LeaveFullscreenButton.h b/src/libpentobi_gui/LeaveFullscreenButton.h new file mode 100644 index 0000000..19d85cd --- /dev/null +++ b/src/libpentobi_gui/LeaveFullscreenButton.h @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/LeaveFullscreenButton.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_LEAVE_FULLSCREEN_BUTTON_H +#define LIBPENTOBI_GUI_LEAVE_FULLSCREEN_BUTTON_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +class QAction; +class QPropertyAnimation; +class QTimer; +class QToolButton; + +//----------------------------------------------------------------------------- + +/** A button at the top right of the screen to leave fullscreen mode that + slides of the screen after a few seconds. + A few pixels of the button stay visible and also an invisible slightly + larger trigger area. If the mouse is moved over this area, the button + becomes visible again. */ +class LeaveFullscreenButton + : public QObject +{ + Q_OBJECT + +public: + /** Constructor. + @param parent The widget that will become fullscreen. This class adds + two child widgets to the parent: the actual button and the trigger area + (an invisible widget that listens for mouse movements and triggers the + button to become visible again if it is slid out). + @param action The action for leaving fullscreen mode associated with + the button */ + LeaveFullscreenButton(QWidget* parent, QAction* action); + + bool eventFilter(QObject* watched, QEvent* event) override; + + void showButton(); + + void hideButton(); + +private: + QToolButton* m_button; + + QWidget* m_triggerArea; + + QPoint m_buttonPos; + + QTimer* m_timer; + + QPropertyAnimation* m_animation; + +private slots: + void slideOut(); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_LEAVE_FULLSCREEN_BUTTON_H diff --git a/src/libpentobi_gui/LineEdit.cpp b/src/libpentobi_gui/LineEdit.cpp new file mode 100644 index 0000000..34d6d85 --- /dev/null +++ b/src/libpentobi_gui/LineEdit.cpp @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/LineEdit.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "LineEdit.h" + +#include + +//----------------------------------------------------------------------------- + +LineEdit::LineEdit(int nuCharactersHint) + : m_nuCharactersHint(nuCharactersHint) +{ +} + +QSize LineEdit::sizeHint() const +{ + QFont font = QApplication::font(); + QFontMetrics metrics(font); + QSize size = QLineEdit::sizeHint(); + size.setWidth(m_nuCharactersHint * metrics.averageCharWidth()); + return size; +} + +//----------------------------------------------------------------------------- + diff --git a/src/libpentobi_gui/LineEdit.h b/src/libpentobi_gui/LineEdit.h new file mode 100644 index 0000000..3dbf421 --- /dev/null +++ b/src/libpentobi_gui/LineEdit.h @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/LineEdit.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_LINE_EDIT_H +#define LIBPENTOBI_GUI_LINE_EDIT_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +//----------------------------------------------------------------------------- + +/** QLineEdit with a configurable size hint depending on the expected + number of characters. */ +class LineEdit + : public QLineEdit +{ + Q_OBJECT + +public: + explicit LineEdit(int nuCharactersHint); + + QSize sizeHint() const override; + +private: + int m_nuCharactersHint; +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_LINE_EDIT_H diff --git a/src/libpentobi_gui/OrientationDisplay.cpp b/src/libpentobi_gui/OrientationDisplay.cpp new file mode 100644 index 0000000..19dcfbd --- /dev/null +++ b/src/libpentobi_gui/OrientationDisplay.cpp @@ -0,0 +1,218 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/OrientationDisplay.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "OrientationDisplay.h" + +#include +#include "libboardgame_base/GeometryUtil.h" +#include "libpentobi_gui/Util.h" + +using namespace std; +using libboardgame_base::ArrayList; +using libboardgame_base::CoordPoint; +using libboardgame_base::Transform; +using libboardgame_base::geometry_util::normalize_offset; +using libboardgame_base::geometry_util::type_match_offset; +using libboardgame_base::geometry_util::type_match_shift; +using libpentobi_base::Geometry; +using libpentobi_base::PiecePoints; +using libpentobi_base::PieceSet; + +//----------------------------------------------------------------------------- + +OrientationDisplay::OrientationDisplay(QWidget* parent, const Board& bd) + : QWidget(parent), + m_bd(bd) +{ + setMinimumSize(30, 30); +} + +void OrientationDisplay::clearSelectedColor() +{ + if (m_isColorSelected) + { + m_isColorSelected = false; + update(); + } +} + +void OrientationDisplay::clearPiece() +{ + if (m_piece.is_null()) + return; + m_piece = Piece::null(); + update(); +} + +void OrientationDisplay::mousePressEvent(QMouseEvent*) +{ + if (m_isColorSelected && m_piece.is_null()) + emit colorClicked(m_color); +} + +void OrientationDisplay::paintEvent(QPaintEvent*) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + auto variant = m_bd.get_variant(); + qreal fieldWidth; + qreal fieldHeight; + qreal displayWidth; + qreal displayHeight; + auto pieceSet = m_bd.get_piece_set(); + bool isTrigon = (pieceSet == PieceSet::trigon); + bool isNexos = (pieceSet == PieceSet::nexos); + bool isCallisto = (pieceSet == PieceSet::callisto); + qreal ratio; + int columns; + int rows; + if (isTrigon) + { + ratio = 1.732; + columns = 7; + rows = 4; + } + else if (isNexos) + { + ratio = 1; + columns = 8; + rows = 8; + } + else + { + ratio = 1; + columns = 5; + rows = 5; + } + fieldWidth = min(qreal(width()) / columns, + qreal(height()) / (ratio * rows)); + if (fieldWidth > 8) + // Prefer pixel alignment if piece is not too small + fieldWidth = floor(fieldWidth); + fieldHeight = ratio * fieldWidth; + displayWidth = fieldWidth * columns; + displayHeight = fieldHeight * rows; + if (m_piece.is_null()) + { + if (m_isColorSelected) + { + qreal dotSize = 0.07 * height(); + QColor color = Util::getPaintColor(variant, m_color); + painter.setBrush(color); + painter.setPen(Qt::NoPen); + painter.drawEllipse(QPointF(0.5 * width(), 0.5 * height()), + dotSize, dotSize); + } + return; + } + painter.save(); + painter.translate(0.5 * (width() - displayWidth), + 0.5 * (height() - displayHeight)); + PiecePoints points = m_bd.get_piece_info(m_piece).get_points(); + m_transform->transform(points.begin(), points.end()); + auto& geo = m_bd.get_geometry(); + type_match_shift(geo, points.begin(), points.end(), + m_transform->get_new_point_type()); + unsigned width; + unsigned height; + CoordPoint offset; + normalize_offset(points.begin(), points.end(), width, height, offset); + offset = type_match_offset(geo, geo.get_point_type(offset)); + painter.save(); + painter.translate(0.5 * (displayWidth - width * fieldWidth), + 0.5 * (displayHeight - height * fieldHeight)); + ArrayList junctions; + for (CoordPoint p : points) + { + qreal x = p.x * fieldWidth; + qreal y = p.y * fieldHeight; + auto pointType = geo.get_point_type(p + offset); + if (isTrigon) + { + bool isUpward = (pointType == 0); + Util::paintColorTriangle(painter, variant, m_color, isUpward, + x, y, fieldWidth, fieldHeight); + } + else if (isNexos) + { + if (pointType == 1 || pointType == 2) + { + bool isHorizontal = (pointType == 1); + Util::paintColorSegment(painter, variant, m_color, + isHorizontal, x, y, fieldWidth); + if (pointType == 1) // Horiz. segment + { + junctions.include(CoordPoint(p.x - 1, p.y)); + junctions.include(CoordPoint(p.x + 1, p.y)); + } + else + { + LIBBOARDGAME_ASSERT(pointType == 2); // Vert. segment + junctions.include(CoordPoint(p.x, p.y - 1)); + junctions.include(CoordPoint(p.x, p.y + 1)); + } + } + } + else if (isCallisto) + { + bool hasRight = points.contains(CoordPoint(p.x + 1, p.y)); + bool hasDown = points.contains(CoordPoint(p.x, p.y + 1)); + bool isOnePiece = (points.size() == 1); + Util::paintColorSquareCallisto(painter, variant, m_color, x, y, + fieldWidth, hasRight, hasDown, + isOnePiece); + } + else + Util::paintColorSquare(painter, variant, m_color, x, y, + fieldWidth); + } + if (isNexos) + for (CoordPoint p : junctions) + { + bool hasLeft = points.contains(CoordPoint(p.x - 1, p.y)); + bool hasRight = points.contains(CoordPoint(p.x + 1, p.y)); + bool hasUp = points.contains(CoordPoint(p.x, p.y - 1)); + bool hasDown = points.contains(CoordPoint(p.x, p.y + 1)); + Util::paintJunction(painter, variant, m_color, p.x * fieldWidth, + p.y * fieldHeight, fieldWidth, fieldHeight, + hasLeft, hasRight, hasUp, hasDown); + } + painter.restore(); + painter.restore(); +} + +void OrientationDisplay::selectColor(Color c) +{ + if (m_isColorSelected && m_color == c) + return; + m_isColorSelected = true; + m_color = c; + update(); +} + +void OrientationDisplay::setSelectedPiece(Piece piece) +{ + auto transform = m_bd.get_transforms().get_default(); + if (m_piece == piece && m_transform == transform) + return; + m_piece = piece; + m_transform = transform; + update(); +} + +void OrientationDisplay::setSelectedPieceTransform(const Transform* transform) +{ + if (m_transform == transform) + return; + m_transform = transform; + update(); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/OrientationDisplay.h b/src/libpentobi_gui/OrientationDisplay.h new file mode 100644 index 0000000..ba28eda --- /dev/null +++ b/src/libpentobi_gui/OrientationDisplay.h @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/OrientationDisplay.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_ORIENTATION_DISPLAY_H +#define LIBPENTOBI_GUI_ORIENTATION_DISPLAY_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "libpentobi_base/Board.h" + +using libboardgame_base::Transform; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::Piece; +using libpentobi_base::PieceInfo; + +//----------------------------------------------------------------------------- + +class OrientationDisplay + : public QWidget +{ + Q_OBJECT + +public: + OrientationDisplay(QWidget* parent, const Board& bd); + + void selectColor(Color c); + + void clearSelectedColor(); + + void clearPiece(); + + void setSelectedPiece(Piece piece); + + void setSelectedPieceTransform(const Transform* transform); + +signals: + /** A mouse click on the orientation display while a color but no no piece + was selected. */ + void colorClicked(Color color); + +protected: + void mousePressEvent(QMouseEvent* event) override; + + void paintEvent(QPaintEvent* event) override; + +private: + const Board& m_bd; + + Piece m_piece = Piece::null(); + + const Transform* m_transform = nullptr; + + bool m_isColorSelected = false; + + Color m_color; +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_ORIENTATION_DISPLAY_H diff --git a/src/libpentobi_gui/PieceSelector.cpp b/src/libpentobi_gui/PieceSelector.cpp new file mode 100644 index 0000000..d6d3029 --- /dev/null +++ b/src/libpentobi_gui/PieceSelector.cpp @@ -0,0 +1,380 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/PieceSelector.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PieceSelector.h" + +#include +#include +#include "libboardgame_base/GeometryUtil.h" +#include "libboardgame_util/StringUtil.h" +#include "libpentobi_gui/Util.h" + +using libboardgame_base::CoordPoint; +using libboardgame_base::geometry_util::type_match_shift; +using libboardgame_util::trim; +using libpentobi_base::BoardConst; +using libpentobi_base::BoardType; +using libpentobi_base::Geometry; +using libpentobi_base::PieceMap; +using libpentobi_base::PieceSet; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +namespace { + +const char* pieceLayoutCallisto = + " 1 . U U U . O O . O O . L L . L . Z . . Z . . I . I . 2" + " . . U . U . O O . O O . L . . L . Z Z . Z Z . I . I . 2" + " 1 . . . . . . . . . . . L . L L . . Z . . Z . I . I . ." + " . .T5T5T5 . . W . . X . . . . . . . . . . . . . . . . 2" + " 1 . .T5 . . W W . X X X .T4T4T4 .T4T4T4 . V . . V . . 2" + " . . .T5 . W W . . . X . . .T4 . . .T4 . . V V . V V . ."; + +const char* pieceLayoutClassic = + " 1 .Z4Z4 . .L4L4L4 . O O . P P .L5L5L5L5 .V5V5V5 . U U U . N . . ." + " . . .Z4Z4 . . .L4 . O O . P P .L5 . . . .V5 . . . U . U . N N .I5" + " 2 2 . . . .T4 . . . . . . P . . . . X . .V5 .Z5 . . . . . . N .I5" + " . . .I3 .T4T4T4 . . W W . . . F . X X X . . .Z5Z5Z5 . .T5 . N .I5" + "V3 . .I3 . . . . . . . W W . F F . . X . . Y . . .Z5 . .T5 . . .I5" + "V3V3 .I3 . .I4I4I4I4 . . W . . F F . . . Y Y Y Y . . .T5T5T5 . .I5"; + +const char* pieceLayoutJunior = + "1 . 1 . V3V3. . L4L4L4. T4T4T4. . O O . O O . P P . . I5. I5. . L5L5" + ". . . . V3. . . L4. . . . T4. . . O O . O O . P P . . I5. I5. . . L5" + "2 . 2 . . . V3. . . . L4. . . T4. . . . . . . P . . . I5. I5. L5. L5" + "2 . 2 . . V3V3. . L4L4L4. . T4T4T4. . Z4. Z4. . . P . I5. I5. L5. L5" + ". . . . . . . . . . . . . . . . . . Z4Z4. Z4Z4. P P . I5. I5. L5. . " + "I3I3I3. I3I3I3. I4I4I4I4. I4I4I4I4. Z4. . . Z4. P P . . . . . L5L5. "; + +const char* pieceLayoutTrigon = + "L5L5 . . F F F F . .L6L6 . . O O O . . X X X . . .A6A6 . . G G . G . .C4C4 . . Y Y Y Y" + "L5L5 . . F . F . . .L6L6 . . O O O . . X X X . .A6A6A6A6 . . G G G . .C4C4 . . Y Y . ." + " .L5 . . . . . . S . .L6L6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2" + " . . . . . S S S S . . . . . . . .P5P5P5P5 . . .I6I6 . .I5I5I5I5I5 . . W W W W W . . 2" + "C5C5 . . . S . . . . V V . .P6 . . . .P5 . .A4 . .I6I6 . . . . . . . . . . W . . . . ." + "C5C5C5 . . . . V V V V . .P6P6P6P6P6 . . .A4A4A4 . .I6I6 . .I3I3I3 . . 1 . . .I4I4I4I4"; + +// To increase the clickable area and to ensure that the pieces can be found +// in the string with flood filling, the Nexos pieces also include some +// crossable junction points that are not part of the piece definition(they +// will be filtered out before finding the piece). But the number of points per +// piece must be at most PiecePoints::max_size. +const char* pieceLayoutNexos = + " . . F F F F F . . . O O O .U4U4U4U4U4 . . . . N N N N . . . . H H H . .U3 .U3 . . .V2V2V2" + "I4 . . . F . F . Y . O . O .U4 . . .U4 .T4 . . . . . N . . . . . H . . .U3 .U3 . . . . .V2" + "I4 . . . . . . . Y . . O O . . . . . . .T4T4T4T4 . . N N . . . . H H . .U3U3U3 . . . . .V2" + "I4 .L4 . . . . . Y . . . . . . . . . . .T4 . . . . . . . . . X . . . . . . . . . . . J . ." + "I4 .L4 . . . . Y Y .L3 . G G . . . . . . . . . . .Z3Z3 . . X X X . . . . . .I2 . . . J . ." + "I4 .L4 . W . . . Y .L3 . G . . . E . . . . .T3 . . .Z3 . . . X . . . . .Z4 .I2 . J . J .V4" + "I4 .L4 . W W W . Y .L3 . G G G . E E E E . .T3T3 . .Z3Z3 . . . .Z4Z4Z4Z4Z4 .I2 . J J J .V4" + "I4 .L4 . . . W . . .L3 . . . G . . . E . . .T3 . . . . . . . . .Z4 . . . . .I2 . . . . .V4" + " . .L4L4 . . W W . .L3L3L3 . . . . . . . . . . . .I3I3I3I3I3 . . . . 1 1 1 .I2 . .V4V4V4V4"; + +} // namespace + +//----------------------------------------------------------------------------- + +PieceSelector::PieceSelector(QWidget* parent, const Board& bd, Color color) + : QWidget(parent), + m_bd(bd), + m_color(color) +{ + setMinimumSize(170, 30); + init(); +} + +void PieceSelector::checkUpdate() +{ + bool disabledStatus[maxColumns][maxRows]; + setDisabledStatus(disabledStatus); + for (unsigned x = 0; x < m_nuColumns; ++x) + for (unsigned y = 0; y < m_nuRows; ++y) + if (! m_piece[x][y].is_null() + && disabledStatus[x][y] != m_disabled[x][y]) + { + update(); + return; + } +} + +void PieceSelector::filterCrossableJunctions(PiecePoints& points) const +{ + auto& geo = m_bd.get_geometry(); + PiecePoints newPoints; + for (auto& p : points) + { + if (geo.get_point_type(p) != 0) + // Not a junction + newPoints.push_back(p); + else if (points.contains(CoordPoint(p.x - 1, p.y)) + && points.contains(CoordPoint(p.x + 1, p.y)) + && ! points.contains(CoordPoint(p.x, p.y - 1)) + && ! points.contains(CoordPoint(p.x, p.y + 1))) + // Necessary junction + newPoints.push_back(p); + else if (! points.contains(CoordPoint(p.x - 1, p.y)) + && ! points.contains(CoordPoint(p.x + 1, p.y)) + && points.contains(CoordPoint(p.x, p.y - 1)) + && points.contains(CoordPoint(p.x, p.y + 1))) + // Necessary junction + newPoints.push_back(p); + } + points = newPoints; +} + +void PieceSelector::findPiecePoints(Piece piece, unsigned x, unsigned y, + PiecePoints& points) const +{ + CoordPoint p(x, y); + if (x >= m_nuColumns || y >= m_nuRows || m_piece[x][y] != piece + || points.contains(p)) + return; + points.push_back(p); + // This assumes that no Trigon pieces touch at the corners, otherwise + // we would need to iterate over neighboring CoordPoint's corresponding to + // Geometry::get_adj() + findPiecePoints(piece, x + 1, y, points); + findPiecePoints(piece, x - 1, y, points); + findPiecePoints(piece, x, y + 1, points); + findPiecePoints(piece, x, y - 1, points); +} + +void PieceSelector::init() +{ + auto pieceSet = m_bd.get_piece_set(); + switch (pieceSet) + { + case PieceSet::classic: + m_pieceLayout = pieceLayoutClassic; + m_nuColumns = 33; + m_nuRows = 6; + break; + case PieceSet::trigon: + m_pieceLayout = pieceLayoutTrigon; + m_nuColumns = 43; + m_nuRows = 6; + break; + case PieceSet::junior: + m_pieceLayout = pieceLayoutJunior; + m_nuColumns = 34; + m_nuRows = 6; + break; + case PieceSet::nexos: + m_pieceLayout = pieceLayoutNexos; + m_nuColumns = 45; + m_nuRows = 9; + break; + case PieceSet::callisto: + m_pieceLayout = pieceLayoutCallisto; + m_nuColumns = 28; + m_nuRows = 6; + break; + } + LIBBOARDGAME_ASSERT(m_nuColumns <= maxColumns); + LIBBOARDGAME_ASSERT(m_nuRows <= maxRows); + for (unsigned y = 0; y < m_nuRows; ++y) + for (unsigned x = 0; x < m_nuColumns; ++x) + { + string name = m_pieceLayout.substr(y * m_nuColumns * 2 + x * 2, 2); + name = trim(name); + Piece piece = Piece::null(); + if (name != ".") + { + m_bd.get_piece_by_name(name, piece); + LIBBOARDGAME_ASSERT(! piece.is_null()); + } + m_piece[x][y] = piece; + } + auto& geo = m_bd.get_geometry(); + for (unsigned y = 0; y < m_nuRows; ++y) + for (unsigned x = 0; x < m_nuColumns; ++x) + { + Piece piece = m_piece[x][y]; + if (piece.is_null()) + continue; + PiecePoints points; + findPiecePoints(piece, x, y, points); + // We need to match the coordinate system of the piece selector to + // the geometry, they are different in Trigon3. + type_match_shift(geo, points.begin(), points.end(), 0); + if (pieceSet == PieceSet::nexos) + filterCrossableJunctions(points); + m_transform[x][y] = + m_bd.get_piece_info(piece).find_transform(geo, points); + LIBBOARDGAME_ASSERT(m_transform[x][y]); + } + setDisabledStatus(m_disabled); + update(); +} + +void PieceSelector::mousePressEvent(QMouseEvent* event) +{ + qreal pixelX = event->x() - 0.5 * (width() - m_selectorWidth); + qreal pixelY = event->y() - 0.5 * (height() - m_selectorHeight); + if (pixelX < 0 || pixelX >= m_selectorWidth + || pixelY < 0 || pixelY >= m_selectorHeight) + return; + int x = static_cast(pixelX / m_fieldWidth); + int y = static_cast(pixelY / m_fieldHeight); + Piece piece = m_piece[x][y]; + if (piece.is_null() || m_disabled[x][y]) + return; + update(); + emit pieceSelected(m_color, piece, m_transform[x][y]); +} + +void PieceSelector::paintEvent(QPaintEvent*) +{ + setDisabledStatus(m_disabled); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + auto pieceSet = m_bd.get_piece_set(); + bool isTrigon = (pieceSet == PieceSet::trigon); + bool isNexos = (pieceSet == PieceSet::nexos); + bool isCallisto = (pieceSet == PieceSet::callisto); + qreal ratio; + if (isTrigon) + { + ratio = 1.732; + m_fieldWidth = min(qreal(width()) / (m_nuColumns + 1), + qreal(height()) / (ratio * m_nuRows)); + } + else + { + ratio = 1; + m_fieldWidth = min(qreal(width()) / m_nuColumns, + qreal(height()) / m_nuRows); + } + if (m_fieldWidth > 8) + // Prefer pixel alignment if piece is not too small + m_fieldWidth = floor(m_fieldWidth); + m_fieldHeight = ratio * m_fieldWidth; + m_selectorWidth = m_fieldWidth * m_nuColumns; + m_selectorHeight = m_fieldHeight * m_nuRows; + painter.save(); + painter.translate(0.5 * (width() - m_selectorWidth), + 0.5 * (height() - m_selectorHeight)); + auto variant = m_bd.get_variant(); + auto& geo = m_bd.get_geometry(); + for (unsigned x = 0; x < m_nuColumns; ++x) + for (unsigned y = 0; y < m_nuRows; ++y) + { + auto pointType = geo.get_point_type(x, y); + Piece piece = m_piece[x][y]; + if (isTrigon) + { + if (piece.is_null() || m_disabled[x][y]) + continue; + bool isUpward = (pointType == geo.get_point_type(0, 0)); + Util::paintColorTriangle(painter, variant, m_color, isUpward, + x * m_fieldWidth, y * m_fieldHeight, + m_fieldWidth, m_fieldHeight); + } + else if (isNexos) + { + if (pointType == 1 || pointType == 2) + { + if (piece.is_null() || m_disabled[x][y]) + continue; + bool isHorizontal = (geo.get_point_type(x, y) == 1); + Util::paintColorSegment(painter, variant, m_color, + isHorizontal, x * m_fieldWidth, + y * m_fieldHeight, m_fieldWidth); + } + else if (pointType == 0) + { + bool hasLeft = + (x > 0 && ! m_piece[x - 1][y].is_null() + && ! m_disabled[x - 1][y]); + bool hasRight = + (x < m_nuColumns - 1 + && ! m_piece[x + 1][y].is_null() + && ! m_disabled[x + 1][y]); + bool hasUp = + (y > 0 && ! m_piece[x][y - 1].is_null() + && ! m_disabled[x][y - 1]); + bool hasDown = + (y < m_nuRows - 1 + && ! m_piece[x][y + 1].is_null() + && ! m_disabled[x][y + 1]); + Util::paintJunction(painter, variant, m_color, + x * m_fieldWidth, y * m_fieldHeight, + m_fieldWidth, m_fieldHeight, hasLeft, + hasRight, hasUp, hasDown); + } + } + else + { + if (piece.is_null() || m_disabled[x][y]) + continue; + if (isCallisto) + { + bool hasLeft = (x > 0 && ! m_piece[x - 1][y].is_null()); + bool hasRight = + (x < m_nuColumns - 1 + && ! m_piece[x + 1][y].is_null()); + bool hasUp = (y > 0 && ! m_piece[x][y - 1].is_null()); + bool hasDown = + (y < m_nuRows - 1 + && ! m_piece[x][y + 1].is_null()); + bool isOnePiece = + (! hasLeft && ! hasRight && ! hasUp && ! hasDown); + Util::paintColorSquareCallisto(painter, variant, m_color, + x * m_fieldWidth, + y * m_fieldHeight, + m_fieldWidth, hasRight, + hasDown, isOnePiece); + } + else + Util::paintColorSquare(painter, variant, m_color, + x * m_fieldWidth, y * m_fieldHeight, + m_fieldWidth); + } + } + painter.restore(); +} + +void PieceSelector::setDisabledStatus(bool disabledStatus[maxColumns][maxRows]) +{ + bool marker[maxColumns][maxRows]; + for (unsigned x = 0; x < m_nuColumns; ++x) + for (unsigned y = 0; y < m_nuRows; ++y) + { + marker[x][y] = false; + disabledStatus[x][y] = false; + } + PieceMap nuInstances; + nuInstances.fill(0); + bool isColorUsed = (m_color.to_int() < m_bd.get_nu_colors()); + for (unsigned x = 0; x < m_nuColumns; ++x) + for (unsigned y = 0; y < m_nuRows; ++y) + { + if (marker[x][y]) + continue; + Piece piece = m_piece[x][y]; + if (piece.is_null()) + continue; + PiecePoints points; + findPiecePoints(piece, x, y, points); + bool disabled = false; + if (! isColorUsed + || ++nuInstances[piece] > m_bd.get_nu_left_piece(m_color, + piece)) + disabled = true; + for (auto& p : points) + { + disabledStatus[p.x][p.y] = disabled; + marker[p.x][p.y] = true; + } + } +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/PieceSelector.h b/src/libpentobi_gui/PieceSelector.h new file mode 100644 index 0000000..2f49ebc --- /dev/null +++ b/src/libpentobi_gui/PieceSelector.h @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/PieceSelector.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_PIECE_SELECTOR_H +#define LIBPENTOBI_GUI_PIECE_SELECTOR_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include "libpentobi_base/Board.h" +#include "libpentobi_base/Color.h" + +using namespace std; +using libboardgame_base::Transform; +using libboardgame_util::ArrayList; +using libpentobi_base::Color; +using libpentobi_base::Board; +using libpentobi_base::Piece; +using libpentobi_base::PiecePoints; + +//----------------------------------------------------------------------------- + +class PieceSelector + : public QWidget +{ + Q_OBJECT + +public: + PieceSelector(QWidget* parent, const Board& bd, Color color); + + /** Needs to be called after the game variant of the current board has + changed because references to pieces are only unique within a + game variant. */ + void init(); + + /** Call update() if pieces left have changed since last paint. */ + void checkUpdate(); + +signals: + void pieceSelected(Color color, Piece piece, const Transform* transform); + +protected: + void mousePressEvent(QMouseEvent* event) override; + + void paintEvent(QPaintEvent* event) override; + +private: + static const unsigned maxColumns = 45; + + static const unsigned maxRows = 9; + + const Board& m_bd; + + Color m_color; + + unsigned m_nuColumns; + + unsigned m_nuRows; + + Piece m_piece[maxColumns][maxRows]; + + const Transform* m_transform[maxColumns][maxRows]; + + bool m_disabled[maxColumns][maxRows]; + + qreal m_fieldWidth; + + qreal m_fieldHeight; + + qreal m_selectorWidth; + + qreal m_selectorHeight; + + string m_pieceLayout; + + + void filterCrossableJunctions(PiecePoints& points) const; + + void findPiecePoints(Piece piece, unsigned x, unsigned y, + PiecePoints& points) const; + + void setDisabledStatus(bool disabledStatus[maxColumns][maxRows]); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_PIECE_SELECTOR_H diff --git a/src/libpentobi_gui/SameHeightLayout.cpp b/src/libpentobi_gui/SameHeightLayout.cpp new file mode 100644 index 0000000..b61d887 --- /dev/null +++ b/src/libpentobi_gui/SameHeightLayout.cpp @@ -0,0 +1,116 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/SameHeightLayout.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SameHeightLayout.h" + +#include +#include + +using namespace std; + +//----------------------------------------------------------------------------- + +SameHeightLayout::SameHeightLayout(QWidget* parent) + : QLayout(parent) +{ +} + +SameHeightLayout::~SameHeightLayout() +{ + QLayoutItem* item; + while ((item = takeAt(0))) + delete item; +} + +void SameHeightLayout::addItem(QLayoutItem* item) +{ + m_list.append(item); +} + +QSize SameHeightLayout::sizeHint() const +{ + QSize s(0, 0); + int count = m_list.count(); + int i = 0; + while (i < count) + { + QSize size = m_list.at(i)->sizeHint(); + s.setWidth(max(size.width(), s.width())); + s.setHeight(s.height() + size.height()); + ++i; + } + return s + (count - 1) * QSize(0, getSpacing()); +} + +QSize SameHeightLayout::minimumSize() const +{ + QSize s(0, 0); + int count = m_list.count(); + int i = 0; + while (i < count) + { + QSize size = m_list.at(i)->minimumSize(); + s.setWidth(max(size.width(), s.width())); + s.setHeight(s.height() + size.height()); + ++i; + } + return s + (count - 1) * QSize(0, getSpacing()); +} + +int SameHeightLayout::count() const +{ + return m_list.size(); +} + +int SameHeightLayout::getSpacing() const +{ + // spacing() returns -1 with Qt 4.7 on KDE. It returns 6 on Gnome. Is this a + // bug? The documentation says: "If no value is explicitly set, the layout's + // spacing is inherited from the parent layout, or from the style settings + // for the parent widget." + int result = spacing(); + if (result < 0 && parentWidget()) + result = parentWidget()->style()->layoutSpacing(QSizePolicy::Frame, + QSizePolicy::Frame, + Qt::Vertical); + if (result < 0) + result = 5; + return result; +} + +QLayoutItem* SameHeightLayout::itemAt(int i) const +{ + return m_list.value(i); +} + +QLayoutItem* SameHeightLayout::takeAt(int i) +{ + return i >= 0 && i < m_list.size() ? m_list.takeAt(i) : nullptr; +} + +void SameHeightLayout::setGeometry(const QRect& rect) +{ + QLayout::setGeometry(rect); + if (m_list.size() == 0) + return; + int count = m_list.count(); + int width = rect.width(); + int height = (rect.height() - (count - 1) * getSpacing()) / count; + int x = rect.x(); + int y = rect.y(); + for (int i = 0; i < count; ++i) + { + QRect geom(x, y, width, height); + m_list.at(i)->setGeometry(geom); + y = y + height + getSpacing(); + } +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/SameHeightLayout.h b/src/libpentobi_gui/SameHeightLayout.h new file mode 100644 index 0000000..e2bf5ff --- /dev/null +++ b/src/libpentobi_gui/SameHeightLayout.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/SameHeightLayout.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_SAME_HEIGHT_LAYOUT_H +#define LIBPENTOBI_GUI_SAME_HEIGHT_LAYOUT_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +//----------------------------------------------------------------------------- + +/** Layout that assigns exactly the same height to all items. + Needed for the box containing the piece selectors, because QBoxLayout + and QGridLayout do not always assign the exact same height to all items + if the height is not a multiple of the number of items. */ +class SameHeightLayout + : public QLayout +{ + Q_OBJECT + +public: + explicit SameHeightLayout(QWidget* parent = nullptr); + + ~SameHeightLayout(); + + void addItem(QLayoutItem* item) override; + + QSize sizeHint() const override; + + QSize minimumSize() const override; + + int count() const override; + + QLayoutItem* itemAt(int i) const override; + + QLayoutItem* takeAt(int i) override; + + void setGeometry(const QRect& rect) override; + +private: + QList m_list; + + int getSpacing() const; +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_SAME_HEIGHT_LAYOUT_H diff --git a/src/libpentobi_gui/ScoreDisplay.cpp b/src/libpentobi_gui/ScoreDisplay.cpp new file mode 100644 index 0000000..e6208f4 --- /dev/null +++ b/src/libpentobi_gui/ScoreDisplay.cpp @@ -0,0 +1,314 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/ScoreDisplay.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "ScoreDisplay.h" + +#include +#include +#include +#include "libpentobi_gui/Util.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +ScoreDisplay::ScoreDisplay(QWidget* parent) + : QWidget(parent) +{ + m_variant = Variant::classic; + m_font.setStyleStrategy(QFont::StyleStrategy(QFont::PreferOutline + | QFont::PreferQuality)); + setMinimumSize(300, 20); + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); +} + +void ScoreDisplay::drawScore(QPainter& painter, Color c, int x) +{ + QColor color = Util::getPaintColor(m_variant, c); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + QFontMetrics metrics(m_font); + int ascent = metrics.ascent(); + // y is baseline + int y = static_cast(ceil(0.5 * (height() - ascent)) + ascent); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.drawEllipse(x, y - m_colorDotSize, m_colorDotSize, + m_colorDotSize); + QString text = getScoreText(c); + bool underline = ! m_hasMoves[c]; + bool hasBonus = m_bonus[c] != 0; + drawText(painter, text, x + m_colorDotWidth, y, underline, hasBonus); +} + +void ScoreDisplay::drawScore2(QPainter& painter, Color c1, Color c2, int x) +{ + auto color = Util::getPaintColor(m_variant, c1); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + QFontMetrics metrics(m_font); + int ascent = metrics.ascent(); + // y is baseline + int y = static_cast(ceil(0.5 * (height() - ascent)) + ascent); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.drawEllipse(x, y - m_colorDotSize, m_colorDotSize, m_colorDotSize); + color = Util::getPaintColor(m_variant, c2); + painter.setBrush(color); + painter.drawEllipse(x + m_colorDotSize, y - m_colorDotSize, m_colorDotSize, + m_colorDotSize); + QString text = getScoreText2(c1, c2); + bool underline = (! m_hasMoves[c1] && ! m_hasMoves[c2]); + drawText(painter, text, x + m_twoColorDotWidth, y, underline, false); +} + +void ScoreDisplay::drawScore3(QPainter& painter, int x) +{ + auto color = Util::getPaintColor(m_variant, Color(3)); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + QFontMetrics metrics(m_font); + int ascent = metrics.ascent(); + // y is baseline + int y = static_cast(ceil(0.5 * (height() - ascent)) + ascent); + painter.setRenderHint(QPainter::Antialiasing, true); + if (m_hasMoves[Color(3)]) + { + painter.drawEllipse(x, y - m_colorDotSize, + m_colorDotSize, m_colorDotSize); + color = Util::getPaintColor(m_variant, m_altPlayer); + painter.setBrush(color); + painter.drawEllipse(x + m_colorDotSize, y - m_colorDotSize, + m_colorDotSize, m_colorDotSize); + } + else + painter.drawEllipse(x + m_colorDotSize, y - m_colorDotSize, + m_colorDotSize, m_colorDotSize); + QString text = getScoreText3(); + bool underline = ! m_hasMoves[Color(3)]; + drawText(painter, text, x + m_twoColorDotWidth, y, underline, false); +} + +void ScoreDisplay::drawText(QPainter& painter, const QString& text, int x, + int y, bool underline, bool hasBonus) +{ + painter.setFont(m_font); + QFontMetrics metrics(m_font); + auto color = QApplication::palette().color(QPalette::WindowText); + painter.setPen(color); + painter.setRenderHint(QPainter::Antialiasing, false); + painter.drawText(x, y, text); + if (underline) + { + // Draw underline (instead of using an underlined font because the + // underline of some fonts is too close to the text and we want it + // to be very visible) + int lineWidth = metrics.lineWidth(); + QPen pen(color); + pen.setWidth(lineWidth); + painter.setPen(pen); + y += 2 * lineWidth; + if (y > height() - 1) + y = height() - 1; + painter.drawLine(x + (hasBonus ? metrics.width(text.left(1)) : 0), y, + x + metrics.width(text), y); + } +} + +QString ScoreDisplay::getScoreText(ScoreType points, ScoreType bonus) const +{ + return QString("%1%2").arg(bonus > 0 ? "*" : "", QString::number(points)); +} + +QString ScoreDisplay::getScoreText(Color c) +{ + return getScoreText(m_points[c], m_bonus[c]); +} + +QString ScoreDisplay::getScoreText2(Color c1, Color c2) +{ + return getScoreText(m_points[c1] + m_points[c2], 0); +} + +QString ScoreDisplay::getScoreText3() +{ + return "(" + getScoreText(Color(3)) + ")"; +} + +int ScoreDisplay::getScoreTextWidth(Color c) +{ + return getTextWidth(getScoreText(c)); +} + +int ScoreDisplay::getScoreTextWidth2(Color c1, Color c2) +{ + return getTextWidth(getScoreText2(c1, c2)); +} + +int ScoreDisplay::getScoreTextWidth3() +{ + return getTextWidth(getScoreText3()); +} + +int ScoreDisplay::getTextWidth(QString text) const +{ + // Make text width only depend on number of digits to avoid frequent small + // changes to the layout + QFontMetrics metrics(m_font); + int maxDigitWidth = 0; + maxDigitWidth = max(maxDigitWidth, metrics.width('0')); + maxDigitWidth = max(maxDigitWidth, metrics.width('1')); + maxDigitWidth = max(maxDigitWidth, metrics.width('2')); + maxDigitWidth = max(maxDigitWidth, metrics.width('3')); + maxDigitWidth = max(maxDigitWidth, metrics.width('4')); + maxDigitWidth = max(maxDigitWidth, metrics.width('5')); + maxDigitWidth = max(maxDigitWidth, metrics.width('6')); + maxDigitWidth = max(maxDigitWidth, metrics.width('7')); + maxDigitWidth = max(maxDigitWidth, metrics.width('8')); + maxDigitWidth = max(maxDigitWidth, metrics.width('9')); + return max(text.length() * maxDigitWidth, + metrics.boundingRect(text).width()); +} + +void ScoreDisplay::paintEvent(QPaintEvent*) +{ + QPainter painter(this); + m_colorDotSize = static_cast(0.8 * m_fontSize); + m_colorDotSpace = static_cast(0.3 * m_fontSize); + m_colorDotWidth = m_colorDotSize + m_colorDotSpace; + m_twoColorDotWidth = 2 * m_colorDotSize + m_colorDotSpace; + auto nuColors = get_nu_colors(m_variant); + auto nuPlayers = get_nu_players(m_variant); + if (nuColors == 2) + { + int textWidthBlue = getScoreTextWidth(Color(0)); + int textWidthGreen = getScoreTextWidth(Color(1)); + int totalWidth = textWidthBlue + textWidthGreen + 2 * m_colorDotWidth; + qreal pad = qreal(width() - totalWidth) / 3.f; + qreal x = pad; + drawScore(painter, Color(0), static_cast(x)); + x += m_colorDotWidth + textWidthBlue + pad; + drawScore(painter, Color(1), static_cast(x)); + } + else if (nuColors == 4 && nuPlayers == 4) + { + int textWidthBlue = getScoreTextWidth(Color(0)); + int textWidthYellow = getScoreTextWidth(Color(1)); + int textWidthRed = getScoreTextWidth(Color(2)); + int textWidthGreen = getScoreTextWidth(Color(3)); + int totalWidth = + textWidthBlue + textWidthRed + textWidthYellow + textWidthGreen + + 4 * m_colorDotWidth; + qreal pad = qreal(width() - totalWidth) / 5.f; + qreal x = pad; + drawScore(painter, Color(0), static_cast(x)); + x += m_colorDotWidth + textWidthBlue + pad; + drawScore(painter, Color(1), static_cast(x)); + x += m_colorDotWidth + textWidthYellow + pad; + drawScore(painter, Color(2), static_cast(x)); + x += m_colorDotWidth + textWidthRed + pad; + drawScore(painter, Color(3), static_cast(x)); + } + else if (nuColors == 4 && nuPlayers == 3) + { + int textWidthBlue = getScoreTextWidth(Color(0)); + int textWidthYellow = getScoreTextWidth(Color(1)); + int textWidthRed = getScoreTextWidth(Color(2)); + int textWidthGreen = getScoreTextWidth3(); + int totalWidth = + textWidthBlue + textWidthRed + textWidthYellow + textWidthGreen + + 3 * m_colorDotWidth + m_twoColorDotWidth; + qreal pad = qreal(width() - totalWidth) / 5.f; + qreal x = pad; + drawScore(painter, Color(0), static_cast(x)); + x += m_colorDotWidth + textWidthBlue + pad; + drawScore(painter, Color(1), static_cast(x)); + x += m_colorDotWidth + textWidthYellow + pad; + drawScore(painter, Color(2), static_cast(x)); + x += m_colorDotWidth + textWidthRed + pad; + drawScore3(painter, static_cast(x)); + } + else if (nuColors == 3 && nuPlayers == 3) + { + int textWidthBlue = getScoreTextWidth(Color(0)); + int textWidthYellow = getScoreTextWidth(Color(1)); + int textWidthRed = getScoreTextWidth(Color(2)); + int totalWidth = + textWidthBlue + textWidthRed + textWidthYellow + + 3 * m_colorDotWidth; + qreal pad = qreal(width() - totalWidth) / 4.f; + qreal x = pad; + drawScore(painter, Color(0), static_cast(x)); + x += m_colorDotWidth + textWidthBlue + pad; + drawScore(painter, Color(1), static_cast(x)); + x += m_colorDotWidth + textWidthYellow + pad; + drawScore(painter, Color(2), static_cast(x)); + } + else + { + LIBBOARDGAME_ASSERT(nuColors == 4 && nuPlayers == 2); + int textWidthBlueRed = getScoreTextWidth2(Color(0), Color(2)); + int textWidthYellowGreen = getScoreTextWidth2(Color(1), Color(3)); + int textWidthBlue = getScoreTextWidth(Color(0)); + int textWidthYellow = getScoreTextWidth(Color(1)); + int textWidthRed = getScoreTextWidth(Color(2)); + int textWidthGreen = getScoreTextWidth(Color(3)); + int totalWidth = + textWidthBlueRed + textWidthYellowGreen + + textWidthBlue + textWidthRed + textWidthYellow + textWidthGreen + + 2 * m_twoColorDotWidth + 4 * m_colorDotWidth; + qreal pad = qreal(width() - totalWidth) / 7.f; + qreal x = pad; + drawScore2(painter, Color(0), Color(2), static_cast(x)); + x += m_twoColorDotWidth + textWidthBlueRed + pad; + drawScore2(painter, Color(1), Color(3), static_cast(x)); + x += m_twoColorDotWidth + textWidthYellowGreen + pad; + drawScore(painter, Color(0), static_cast(x)); + x += m_colorDotWidth + textWidthBlue + pad; + drawScore(painter, Color(1), static_cast(x)); + x += m_colorDotWidth + textWidthYellow + pad; + drawScore(painter, Color(2), static_cast(x)); + x += m_colorDotWidth + textWidthRed + pad; + drawScore(painter, Color(3), static_cast(x)); + } +} + +void ScoreDisplay::resizeEvent(QResizeEvent*) +{ + // QFont::setPixelSize(0) prints a warning even if it works and the docs + // of Qt 5.3 don't forbid it (unlike QFont::setPointSize(0)). + m_fontSize = max(1, static_cast(floor(0.6 * height()))); + m_font.setPixelSize(m_fontSize); +} + +void ScoreDisplay::updateScore(const Board& bd) +{ + auto variant = bd.get_variant(); + bool hasChanged = (m_variant != variant); + m_variant = variant; + for (Color c : bd.get_colors()) + { + bool hasMoves = bd.has_moves(c); + auto points = bd.get_points(c); + auto bonus = bd.get_bonus(c); + if (hasMoves != m_hasMoves[c] || m_points[c] != points + || m_bonus[c] != points) + { + hasChanged = true; + m_hasMoves[c] = hasMoves; + m_points[c] = points; + m_bonus[c] = bonus; + } + } + if (variant == Variant::classic_3) + m_altPlayer = Color(bd.get_alt_player()); + if (hasChanged) + update(); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/ScoreDisplay.h b/src/libpentobi_gui/ScoreDisplay.h new file mode 100644 index 0000000..19e2861 --- /dev/null +++ b/src/libpentobi_gui/ScoreDisplay.h @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/ScoreDisplay.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_SCORE_DISPLAY_H +#define LIBPENTOBI_GUI_SCORE_DISPLAY_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "libpentobi_base/Board.h" + +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::ColorMap; +using libpentobi_base::ScoreType; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class ScoreDisplay + : public QWidget +{ + Q_OBJECT + +public: + explicit ScoreDisplay(QWidget* parent = nullptr); + + void updateScore(const Board& bd); + +protected: + void paintEvent(QPaintEvent* event) override; + + void resizeEvent(QResizeEvent* event) override; + +private: + int m_fontSize; + + QFont m_font; + + Variant m_variant; + + ColorMap m_hasMoves{false}; + + ColorMap m_points{0}; + + ColorMap m_bonus{0}; + + /** Current player of 4th color in Variant::classic_3. */ + Color m_altPlayer; + + int m_colorDotSize; + + int m_colorDotSpace; + + int m_colorDotWidth; + + int m_twoColorDotWidth; + + + QString getScoreText(Color c); + + QString getScoreText2(Color c1, Color c2); + + QString getScoreText3(); + + int getScoreTextWidth(Color c); + + int getScoreTextWidth2(Color c1, Color c2); + + int getScoreTextWidth3(); + + void drawScore(QPainter& painter, Color c, int x); + + void drawScore2(QPainter& painter, Color c1, Color c2, int x); + + void drawScore3(QPainter& painter, int x); + + QString getScoreText(ScoreType points, ScoreType bonus) const; + + int getTextWidth(QString text) const; + + void drawText(QPainter& painter, const QString& text, int x, int y, + bool underline, bool hasBonus); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_SCORE_DISPLAY_H diff --git a/src/libpentobi_gui/Util.cpp b/src/libpentobi_gui/Util.cpp new file mode 100644 index 0000000..aad3a2f --- /dev/null +++ b/src/libpentobi_gui/Util.cpp @@ -0,0 +1,531 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/Util.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Util.h" + +#include + +//----------------------------------------------------------------------------- + +namespace { + +const QColor blue(0, 115, 207); + +const QColor green(0, 192, 0); + +const QColor red(230, 62, 44); + +const QColor yellow(235, 205, 35); + +const QColor gray(174, 167, 172); + +void setAlphaSaturation(QColor& c, qreal alpha, qreal saturation) +{ + if (saturation != 1) + c.setHsv(c.hue(), static_cast(saturation * c.saturation()), + c.value()); + if (alpha != 1) + c.setAlphaF(alpha); +} + +void paintDot(QPainter& painter, QColor color, qreal x, qreal y, qreal width, + qreal height, qreal size) +{ + painter.save(); + painter.translate(x, y); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawEllipse(QPointF(0.5 * width, 0.5 * height), size, size); + painter.restore(); +} + +void paintSquare(QPainter& painter, qreal x, qreal y, qreal width, + qreal height, const QColor& rectColor, + const QColor& upLeftColor, const QColor& downRightColor, + bool onlyBorder = false) +{ + painter.save(); + painter.translate(x, y); + if (! onlyBorder) + painter.fillRect(QRectF(0, 0, width, height), rectColor); + qreal border = 0.05 * max(width, height); + const QPointF downRightPolygon[6] = + { + QPointF(border, height - border), + QPointF(width - border, height - border), + QPointF(width - border, border), + QPointF(width, 0), + QPointF(width, height), + QPointF(0, height) + }; + painter.setPen(Qt::NoPen); + painter.setBrush(downRightColor); + painter.drawPolygon(downRightPolygon, 6); + const QPointF upLeftPolygon[6] = + { + QPointF(0, 0), + QPointF(width, 0), + QPointF(width - border, border), + QPointF(border, border), + QPointF(border, height - border), + QPointF(0, height) + }; + painter.setBrush(upLeftColor); + painter.drawPolygon(upLeftPolygon, 6); + painter.restore(); +} + +void paintTriangle(QPainter& painter, bool isUpward, qreal x, qreal y, + qreal width, qreal height, const QColor& color, + const QColor& upLeftColor, const QColor& downRightColor) +{ + painter.save(); + painter.translate(x, y); + qreal left = -0.5 * width; + qreal right = 1.5 * width; + if (isUpward) + { + const QPointF polygon[3] = + { + QPointF(left, height), + QPointF(right, height), + QPointF(0.5 * width, 0) + }; + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawConvexPolygon(polygon, 3); + qreal border = 0.08 * width; + const QPointF downRightPolygon[6] = + { + QPointF(left, height), + QPointF(right, height), + QPointF(0.5 * width, 0), + QPointF(0.5 * width, 2 * border), + QPointF(right - 1.732 * border, height - border), + QPointF(left + 1.732 * border, height - border) + }; + painter.setBrush(downRightColor); + painter.drawPolygon(downRightPolygon, 6); + const QPointF upLeftPolygon[4] = + { + QPointF(0.5 * width, 0), + QPointF(0.5 * width, 2 * border), + QPointF(left + 1.732 * border, height - border), + QPointF(left, height), + }; + painter.setBrush(upLeftColor); + painter.drawPolygon(upLeftPolygon, 4); + } + else + { + const QPointF polygon[3] = + { + QPointF(left, 0), + QPointF(right, 0), + QPointF(0.5 * width, height) + }; + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawConvexPolygon(polygon, 3); + qreal border = 0.05 * width; + const QPointF downRightPolygon[4] = + { + QPointF(0.5 * width, height), + QPointF(0.5 * width, height - 2 * border), + QPointF(right - 1.732 * border, border), + QPointF(right, 0) + }; + painter.setBrush(downRightColor); + painter.drawPolygon(downRightPolygon, 4); + const QPointF upLeftPolygon[6] = + { + QPointF(right, 0), + QPointF(right - 1.732 * border, border), + QPointF(left + 1.732 * border, border), + QPointF(0.5 * width, height - 2 * border), + QPointF(0.5 * width, height), + QPointF(left, 0) + }; + painter.setBrush(upLeftColor); + painter.drawPolygon(upLeftPolygon, 6); + } + painter.restore(); +} + +void paintSquareFrame(QPainter& painter, qreal x, qreal y, qreal size, + const QColor& rectColor, const QColor& upLeftColor, + const QColor& downRightColor) +{ + painter.save(); + painter.translate(x, y); + painter.setPen(Qt::NoPen); + qreal border = 0.05 * size; + qreal frameSize = 0.17 * size; + painter.fillRect(QRectF(0, 0, size, frameSize), rectColor); + painter.fillRect(QRectF(0, size - frameSize, size, frameSize), rectColor); + painter.fillRect(QRectF(0, 0, frameSize, size), rectColor); + painter.fillRect(QRectF(size - frameSize, 0, frameSize, size), rectColor); + const QPointF downRightPolygon[6] = + { + QPointF(border, size - border), + QPointF(size - border, size - border), + QPointF(size - border, border), + QPointF(size, 0), + QPointF(size, size), + QPointF(0, size) + }; + painter.setBrush(downRightColor); + painter.drawPolygon(downRightPolygon, 6); + const QPointF upLeftPolygon[6] = + { + QPointF(0, 0), + QPointF(size, 0), + QPointF(size - border, border), + QPointF(border, border), + QPointF(border, size - border), + QPointF(0, size) + }; + painter.setBrush(upLeftColor); + painter.drawPolygon(upLeftPolygon, 6); + painter.restore(); +} + +void paintColorSquareFrame(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size, qreal alpha, + qreal saturation, bool flat) +{ + auto color = Util::getPaintColor(variant, c); + QColor upLeftColor; + QColor downRightColor; + if (flat) + { + upLeftColor = color; + downRightColor = color; + } + else + { + upLeftColor = color.lighter(130); + downRightColor = color.darker(160); + } + setAlphaSaturation(color, alpha, saturation); + setAlphaSaturation(upLeftColor, alpha, saturation); + setAlphaSaturation(downRightColor, alpha, saturation); + paintSquareFrame(painter, x, y, size, color, upLeftColor, downRightColor); +} + +} //namespace + +//----------------------------------------------------------------------------- + +string Util::convertSgfValueFromQString(const QString& value, + const string& charset) +{ + // Is there a way in Qt to support arbitrary Ascii-compatible text + // encodings? Currently, we only support UTF8 (used by Pentobi) and + // treat everything else as ISO-8859-1/Latin1 (the default for SGF) + // even if the charset property specifies some other encoding. + QString charsetToLower = QString(charset.c_str()).trimmed().toLower(); + if (charsetToLower == "utf-8" || charsetToLower == "utf8") + return value.toUtf8().constData(); + else + return value.toLatin1().constData(); +} + +QString Util::convertSgfValueToQString(const string& value, + const string& charset) +{ + // See comment in convertSgfValueFromQString() about supported encodings + QString charsetToLower = QString(charset.c_str()).trimmed().toLower(); + if (charsetToLower == "utf-8" || charsetToLower == "utf8") + return QString::fromUtf8(value.c_str()); + else + return QString::fromLatin1(value.c_str()); +} + +QColor Util::getLabelColor(Variant variant, PointState s) +{ + if (s.is_empty()) + return Qt::black; + Color c = s.to_color(); + QColor paintColor = getPaintColor(variant, c); + if (paintColor == yellow || paintColor == green) + return Qt::black; + else + return Qt::white; +} + +QColor Util::getMarkColor(Variant variant, PointState s) +{ + if (s.is_empty()) + return Qt::white; + Color c = s.to_color(); + QColor paintColor = getPaintColor(variant, c); + if (paintColor == yellow || paintColor == green) + return QColor("#333333"); + else + return Qt::white; +} + +QColor Util::getPaintColor(Variant variant, Color c) +{ + if (get_nu_colors(variant) == 2) + return c == Color(0) ? blue : green; + else + { + if (c == Color(0)) + return blue; + if (c == Color(1)) + return yellow; + if (c == Color(2)) + return red; + LIBBOARDGAME_ASSERT(c == Color(3)); + return green; + } +} + +QString Util::getPlayerString(Variant variant, Color c) +{ + auto i = c.to_int(); + if (get_nu_colors(variant) == 2) + return i == 0 ? qApp->translate("Util", "Blue") + : qApp->translate("Util", "Green"); + if (get_nu_players(variant) == 2) + return i == 0 || i == 2 ? qApp->translate("Util", "Blue/Red") + : qApp->translate("Util", "Yellow/Green"); + if (i == 0) + return qApp->translate("Util", "Blue"); + if (i == 1) + return qApp->translate("Util", "Yellow"); + if (i == 2) + return qApp->translate("Util", "Red"); + return qApp->translate("Util", "Green"); +} + +void Util::paintColorSegment(QPainter& painter, Variant variant, Color c, + bool isHorizontal, qreal x, qreal y, qreal size, + qreal alpha, qreal saturation, bool flat) +{ + auto color = getPaintColor(variant, c); + QColor upLeftColor; + QColor downRightColor; + if (flat) + { + upLeftColor = color; + downRightColor = color; + } + else + { + upLeftColor = color.lighter(130); + downRightColor = color.darker(160); + } + setAlphaSaturation(color, alpha, saturation); + setAlphaSaturation(upLeftColor, alpha, saturation); + setAlphaSaturation(downRightColor, alpha, saturation); + if (isHorizontal) + paintSquare(painter, x - size / 4, y + size / 4, 1.5 * size, size / 2, + color, upLeftColor, downRightColor); + else + paintSquare(painter, x + size / 4, y - size / 4, size / 2, 1.5 * size, + color, upLeftColor, downRightColor); +} + +void Util::paintColorSquare(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size, qreal alpha, + qreal saturation, bool flat) +{ + auto color = getPaintColor(variant, c); + QColor upLeftColor; + QColor downRightColor; + if (flat) + { + upLeftColor = color; + downRightColor = color; + } + else + { + upLeftColor = color.lighter(130); + downRightColor = color.darker(160); + } + setAlphaSaturation(color, alpha, saturation); + setAlphaSaturation(upLeftColor, alpha, saturation); + setAlphaSaturation(downRightColor, alpha, saturation); + paintSquare(painter, x, y, size, size, color, upLeftColor, downRightColor); +} + +void Util::paintColorSquareCallisto(QPainter& painter, Variant variant, + Color c, qreal x, qreal y, qreal size, + bool hasRight, bool hasDown, + bool isOnePiece, qreal alpha, + qreal saturation, bool flat) +{ + auto color = getPaintColor(variant, c); + setAlphaSaturation(color, alpha, saturation); + if (hasRight) + painter.fillRect(QRectF(x + 0.96 * size, y + 0.07 * size, + 0.08 * size, 0.86 * size), color); + if (hasDown) + painter.fillRect(QRectF(x + 0.07 * size, y + 0.96 * size, + 0.86 * size, 0.08 * size), color); + if (isOnePiece) + paintColorSquareFrame(painter, variant, c, x + 0.04 * size, + y + 0.04 * size, 0.92 * size, alpha, saturation, + flat); + else + paintColorSquare(painter, variant, c, x + 0.04 * size, y + 0.04 * size, + 0.92 * size, alpha, saturation, flat); +} + +void Util::paintColorTriangle(QPainter& painter, Variant variant, + Color c, bool isUpward, qreal x, qreal y, + qreal width, qreal height, qreal alpha, + qreal saturation, bool flat) +{ + auto color = getPaintColor(variant, c); + QColor upLeftColor; + QColor downRightColor; + if (flat) + { + upLeftColor = color; + downRightColor = color; + } + else + { + upLeftColor = color.lighter(130); + downRightColor = color.darker(160); + } + setAlphaSaturation(color, alpha, saturation); + setAlphaSaturation(upLeftColor, alpha, saturation); + setAlphaSaturation(downRightColor, alpha, saturation); + paintTriangle(painter, isUpward, x, y, width, height, color, upLeftColor, + downRightColor); +} + +void Util::paintEmptyJunction(QPainter& painter, qreal x, qreal y, qreal size) +{ + painter.fillRect(QRectF(x + 0.25 * size, y + 0.25 * size, + 0.5 * size, 0.5 * size), + gray); +} + +void Util::paintEmptySegment(QPainter& painter, bool isHorizontal, qreal x, + qreal y, qreal size) +{ + if (isHorizontal) + paintSquare(painter, x - size / 4, y + size / 4, 1.5 * size, size / 2, + gray, gray.darker(130), gray.lighter(115)); + else + paintSquare(painter, x + size / 4, y - size / 4, size / 2, 1.5 * size, + gray, gray.darker(130), gray.lighter(115)); +} + +void Util::paintEmptySquare(QPainter& painter, qreal x, qreal y, qreal size) +{ + paintSquare(painter, x, y, size, size, gray, gray.darker(130), + gray.lighter(115)); +} + +void Util::paintEmptySquareCallisto(QPainter& painter, qreal x, qreal y, + qreal size) +{ + painter.fillRect(QRectF(x, y, size, size), gray); + paintSquare(painter, x + 0.04 * size, y + 0.04 * size, 0.92 * size, + 0.92 * size, gray, gray.darker(130), gray.lighter(115), true); +} + +void Util::paintEmptySquareCallistoCenter(QPainter& painter, qreal x, qreal y, + qreal size) +{ + painter.fillRect(QRectF(x, y, size, size), gray); + paintSquare(painter, x + 0.05 * size, y + 0.05 * size, 0.9 * size, + 0.9 * size, gray.darker(120), gray.darker(150), + gray.lighter(95), false); +} + +void Util::paintEmptyTriangle(QPainter& painter, bool isUpward, qreal x, + qreal y, qreal width, qreal height) +{ + paintTriangle(painter, isUpward, x, y, width, height, gray, + gray.darker(130), gray.lighter(115)); +} + +void Util::paintJunction(QPainter& painter, Variant variant, Color c, qreal x, + qreal y, qreal width, qreal height, bool hasLeft, + bool hasRight, bool hasUp, bool hasDown, qreal alpha, + qreal saturation) +{ + auto color = getPaintColor(variant, c); + setAlphaSaturation(color, alpha, saturation); + painter.save(); + painter.translate(x + 0.25 * width, y + 0.25 * height); + width *= 0.5; + height *= 0.5; + if (hasUp && hasDown) + painter.fillRect(QRectF(0.25 * width, 0, 0.5 * width, height), color); + if (hasLeft && hasRight) + painter.fillRect(QRectF(0, 0.25 * height, width, 0.5 * height), color); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + if (hasLeft && hasUp) + { + const QPointF polygon[3] = { QPointF(0, 0), + QPointF(0.75 * width, 0), + QPointF(0, 0.75 * height) }; + painter.drawPolygon(polygon, 3); + } + if (hasRight && hasUp) + { + const QPointF polygon[3] = { QPointF(0.25 * width, 0), + QPointF(width, 0), + QPointF(width, 0.75 * height) }; + painter.drawPolygon(polygon, 3); + } + if (hasLeft && hasDown) + { + const QPointF polygon[3] = { QPointF(0, 0.25 * height), + QPointF(0, height), + QPointF(0.75 * width, height) }; + painter.drawPolygon(polygon, 3); + } + if (hasRight && hasDown) + { + const QPointF polygon[3] = { QPointF(0.25 * width, height), + QPointF(width, 0.25 * height), + QPointF(width, height) }; + painter.drawPolygon(polygon, 3); + } + painter.restore(); +} + +void Util::paintSegmentStartingPoint(QPainter& painter, Variant variant, + Color c, qreal x, qreal y, + qreal size) +{ + paintDot(painter, getPaintColor(variant, c), x, y, size, size, + 0.15 * size); +} + +void Util::paintSquareStartingPoint(QPainter& painter, Variant variant, + Color c, qreal x, qreal y, qreal size) +{ + paintDot(painter, getPaintColor(variant, c), x, y, size, size, + 0.13 * size); +} + +void Util::paintTriangleStartingPoint(QPainter& painter, bool isUpward, + qreal x, qreal y, qreal width, + qreal height) +{ + if (isUpward) + y += 0.333 * height; + height = 0.666 * height; + paintDot(painter, gray.darker(130), x, y, width, height, 0.17 * width); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/Util.h b/src/libpentobi_gui/Util.h new file mode 100644 index 0000000..4a070a3 --- /dev/null +++ b/src/libpentobi_gui/Util.h @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/Util.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_UTIL_H +#define LIBPENTOBI_GUI_UTIL_H + +#include +#include +#include "libpentobi_base/Color.h" +#include "libpentobi_base/Variant.h" +#include "libpentobi_base/PointState.h" + +using namespace std; +using libpentobi_base::Color; +using libpentobi_base::Variant; +using libpentobi_base::PointState; + +//----------------------------------------------------------------------------- + +namespace Util +{ + +QColor getPaintColor(Variant variant, Color c); + +QColor getLabelColor(Variant variant, PointState s); + +QColor getMarkColor(Variant variant, PointState s); + +/** Paint piece segment in Nexos. */ +void paintColorSegment(QPainter& painter, Variant variant, Color c, + bool isHorizontal, qreal x, qreal y, qreal size, + qreal alpha = 1, qreal saturation = 1, + bool flat = false); + +void paintColorSquare(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size, qreal alpha = 1, + qreal saturation = 1, bool flat = false); + +void paintColorSquareCallisto(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size, bool hasRight, + bool hasDown, bool isOnePiece, qreal alpha = 1, + qreal saturation = 1, bool flat = false); + +void paintColorTriangle(QPainter& painter, Variant variant, + Color c, bool isUpward, qreal x, qreal y, qreal width, + qreal height, qreal alpha = 1, qreal saturation = 1, + bool flat = false); + +/** Paint empty junction in Nexos. */ +void paintEmptyJunction(QPainter& painter, qreal x, qreal y, qreal size); + +/** Paint empty segment in Nexos. */ +void paintEmptySegment(QPainter& painter, bool isHorizontal, qreal x, qreal y, + qreal size); + +void paintEmptySquare(QPainter& painter, qreal x, qreal y, qreal size); + +void paintEmptySquareCallisto(QPainter& painter, qreal x, qreal y, qreal size); + +void paintEmptySquareCallistoCenter(QPainter& painter, qreal x, qreal y, + qreal size); + +void paintEmptyTriangle(QPainter& painter, bool isUpward, qreal x, qreal y, + qreal width, qreal height); + +void paintJunction(QPainter& painter, Variant variant, Color c, qreal x, + qreal y, qreal width, qreal height, bool hasLeft, + bool hasRight, bool hasUp, bool hasDown, qreal alpha = 1, + qreal saturation = 1); + +/** Paint starting point in Nexos. */ +void paintSegmentStartingPoint(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size); + +void paintSquareStartingPoint(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size); + +void paintTriangleStartingPoint(QPainter& painter, bool isUpward, qreal x, + qreal y, qreal width, qreal height); + +/** Convert a property value of a SGF tree unto a QString. + @param value + @param charset The value of the CA property of the root node in the tree + or an empty string if the tree has no such property. + This function currently only recognizes UTF8 and ISO-8859-1 (the latter + is the default for SGF if no CA property exists). Other charsets are + ignored and the string is converted using the default system charset. */ +string convertSgfValueFromQString(const QString& value, const string& charset); + +/** Convert a property value of a SGF tree unto a QString. + @param value + @param charset The value of the CA property of the root node in the tree + or an empty string if the tree has no such property. + This function currently only recognizes UTF8 and ISO-8859-1 (the latter + is the default for SGF if no CA property exists). Other charsets are + ignored and the string is converted using the default system charset. */ +QString convertSgfValueToQString(const string& value, const string& charset); + +/** Get a translated string identifying a player, like "Blue" or "Blue/Red". + @param variant The game variant + @param c The player color or one of the player colors in game variants + with multiple colors per player. */ +QString getPlayerString(Variant variant, Color c); + +} + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_UTIL_H diff --git a/src/libpentobi_gui/icons/go-home.svg b/src/libpentobi_gui/icons/go-home.svg new file mode 100644 index 0000000..61e1d58 --- /dev/null +++ b/src/libpentobi_gui/icons/go-home.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/libpentobi_gui/icons/go-next.svg b/src/libpentobi_gui/icons/go-next.svg new file mode 100644 index 0000000..0d797a9 --- /dev/null +++ b/src/libpentobi_gui/icons/go-next.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/libpentobi_gui/icons/go-previous.svg b/src/libpentobi_gui/icons/go-previous.svg new file mode 100644 index 0000000..0aee1b4 --- /dev/null +++ b/src/libpentobi_gui/icons/go-previous.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/libpentobi_gui/libpentobi_gui_resources.qrc b/src/libpentobi_gui/libpentobi_gui_resources.qrc new file mode 100644 index 0000000..0206df4 --- /dev/null +++ b/src/libpentobi_gui/libpentobi_gui_resources.qrc @@ -0,0 +1,8 @@ + + + + icons/go-home.png + icons/go-next.png + icons/go-previous.png + + diff --git a/src/libpentobi_gui/libpentobi_gui_resources_2x.qrc b/src/libpentobi_gui/libpentobi_gui_resources_2x.qrc new file mode 100644 index 0000000..e81e663 --- /dev/null +++ b/src/libpentobi_gui/libpentobi_gui_resources_2x.qrc @@ -0,0 +1,8 @@ + + + + icons/go-home@2x.png + icons/go-next@2x.png + icons/go-previous@2x.png + + diff --git a/src/libpentobi_gui/translations/libpentobi_gui_de.ts b/src/libpentobi_gui/translations/libpentobi_gui_de.ts new file mode 100644 index 0000000..cb3ef0b --- /dev/null +++ b/src/libpentobi_gui/translations/libpentobi_gui_de.ts @@ -0,0 +1,170 @@ + + + + + ComputerColorDialog + + Computer Colors + Computer-Farben + + + Computer plays: + Computer spielt: + + + &Blue + &Blau + + + &Green + &Grün + + + &Yellow + G&elb + + + &Red + &Rot + + + &Blue/Red + &Blau/Rot + + + &Yellow/Green + &Gelb/Grün + + + + GameInfoDialog + + Game Info + Spielinformation + + + Player &Blue: + Player Blue: + Spieler &Blau: + + + Player &Green: + Player Green: + Spieler &Grün: + + + Player &Yellow: + Player Yellow: + Spieler G&elb: + + + Player &Red: + Player Red: + Spieler &Rot: + + + Player &Blue/Red: + Player Blue/Red: + Spieler &Blau/Rot: + + + Player &Yellow/Green: + Player Yellow/Green: + Spieler &Gelb/Grün: + + + &Date: + Date: + &Datum: + + + &Time limits: + Bedenk&zeit: + + + &Event: + &Veranstaltung: + + + R&ound: + R&unde: + + + + HelpWindow + + Back + Zurück + + + Show previous page in history + Die vorherige Seite in der Chronik anzeigen + + + Forward + Vorwärts + + + Show next page in history + Die nächste Seite in der Chronik anzeigen + + + Contents + Inhalt + + + Show table of contents + Das Inhaltsverzeichnis anzeigen + + + + InitialRatingDialog + + Initial Rating + Anfangswertung + + + You have not yet played rated games in this game variant. Estimate your playing strength to initialize your rating. + Sie haben noch keine gewerteten Spiele in dieser Spielvariante gespielt. Schätzen Sie Ihre Spielstärke, um Ihre Wertung zu initialisieren. + + + Beginner + Anfänger + + + Expert + Experte + + + Your initial rating: %1 + Ihre Anfangswertung: %1 + + + + Util + + Blue + Blau + + + Green + Grün + + + Yellow + Gelb + + + Red + Rot + + + Blue/Red + Blau/Rot + + + Yellow/Green + Gelb/Grün + + + diff --git a/src/libpentobi_kde_thumbnailer/CMakeLists.txt b/src/libpentobi_kde_thumbnailer/CMakeLists.txt new file mode 100644 index 0000000..a721e3d --- /dev/null +++ b/src/libpentobi_kde_thumbnailer/CMakeLists.txt @@ -0,0 +1,38 @@ +# libpentobi_kde_thumbnailer contains the files needed by +# the pentobi_kde_thumbnailer plugin compiled with shared library options +# (usually -fPIC) because this is required for building shared libraries on +# some targets (e.g. x86_64). +# +# The alternative would be to add -fPIC to the global compiler flags even for +# executables but this slows down Pentobi's search by 10% on some targets. +# +# Adding the source files directly to pentobi_kde_thumbnailer/CMakeList.txt is +# not possible because the KDE CMake macros add -fno-exceptions to the +# compiler flags, which causes errors in the Pentobi sources that use +# exceptions (which should be fine as long as no exceptions are thrown +# from the thumbnailer plugin functions). + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CMAKE_SHARED_LIBRARY_CXX_FLAGS}") + +add_library(pentobi_kde_thumbnailer STATIC + ../libboardgame_util/Assert.cpp + ../libboardgame_util/Log.cpp + ../libboardgame_util/StringUtil.cpp + ../libboardgame_base/StringRep.cpp + ../libboardgame_sgf/MissingProperty.cpp + ../libboardgame_sgf/Reader.cpp + ../libboardgame_sgf/SgfNode.cpp + ../libboardgame_sgf/SgfTree.cpp + ../libboardgame_sgf/TreeReader.cpp + ../libpentobi_base/CallistoGeometry.cpp + ../libpentobi_base/NexosGeometry.cpp + ../libpentobi_base/NodeUtil.cpp + ../libpentobi_base/StartingPoints.cpp + ../libpentobi_base/TrigonGeometry.cpp + ../libpentobi_base/Variant.cpp + ../libpentobi_gui/BoardPainter.cpp + ../libpentobi_gui/Util.cpp + ../libpentobi_thumbnail/CreateThumbnail.cpp +) + +target_link_libraries(pentobi_kde_thumbnailer Qt5::Widgets) diff --git a/src/libpentobi_mcts/AnalyzeGame.cpp b/src/libpentobi_mcts/AnalyzeGame.cpp new file mode 100644 index 0000000..f4d68b9 --- /dev/null +++ b/src/libpentobi_mcts/AnalyzeGame.cpp @@ -0,0 +1,108 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/AnalyzeGame.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "AnalyzeGame.h" + +#include "Search.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/WallTimeSource.h" + +namespace libpentobi_mcts { + +using libboardgame_sgf::InvalidTree; +using libboardgame_sgf::SgfNode; +using libboardgame_util::clear_abort; +using libboardgame_util::get_abort; +using libboardgame_util::WallTimeSource; +using libpentobi_base::BoardUpdater; + +//----------------------------------------------------------------------------- + +void AnalyzeGame::run(const Game& game, Search& search, size_t nu_simulations, + function progress_callback) +{ + m_variant = game.get_variant(); + m_moves.clear(); + m_values.clear(); + auto& tree = game.get_tree(); + unique_ptr bd(new Board(m_variant)); + BoardUpdater updater; + auto& root = game.get_root(); + auto node = &root; + unsigned total_moves = 0; + try { + while (node) + { + if (tree.has_move(*node)) + ++total_moves; + node = node->get_first_child_or_null(); + } + } + catch (const InvalidTree&) + { + // PentobiTree::has_move() can throw on invalid SGF tree read from + // external file. We simply abort the analysis. + return; + } + WallTimeSource time_source; + clear_abort(); + node = &root; + unsigned move_number = 0; + auto tie_value = Search::SearchParamConst::tie_value; + while (node) + { + auto mv = tree.get_move(*node); + if (! mv.is_null()) + { + if (! node->has_parent()) + { + // Root shouldn't contain moves in SGF files + m_moves.push_back(mv); + m_values.push_back(tie_value); + } + else + { + progress_callback(move_number, total_moves); + try + { + updater.update(*bd, tree, node->get_parent()); + LIBBOARDGAME_LOG("Analyzing move ", bd->get_nu_moves()); + const Float max_count = Float(nu_simulations); + double max_time = 0; + // Set min_simulations to a reasonable value because + // nu_simulations can be reached without having that many + // value updates if a subtree from a previous search is + // reused (which re-initializes the value and value count + // of the new root from the best child) + size_t min_simulations = min(size_t(100), nu_simulations); + Move computer_mv; + search.search(computer_mv, *bd, mv.color, max_count, + min_simulations, max_time, time_source); + if (get_abort()) + break; + m_moves.push_back(mv); + m_values.push_back(search.get_root_val().get_mean()); + } + catch (const InvalidTree&) + { + // BoardUpdater::update() can throw on invalid SGF tree + // read from external file. We simply abort the analysis. + break; + } + } + ++move_number; + } + node = node->get_first_child_or_null(); + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/AnalyzeGame.h b/src/libpentobi_mcts/AnalyzeGame.h new file mode 100644 index 0000000..3b7e601 --- /dev/null +++ b/src/libpentobi_mcts/AnalyzeGame.h @@ -0,0 +1,83 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/AnalyzeGame.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_ANALYZE_GAME_H +#define LIBPENTOBI_MCTS_ANALYZE_GAME_H + +#include +#include +#include "libpentobi_base/Game.h" + +namespace libpentobi_mcts { + +class Search; + +using namespace std; +using libpentobi_base::ColorMove; +using libpentobi_base::Game; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** Evaluate each position in the main variation of a game. */ +class AnalyzeGame +{ +public: + /** Run the analysis. + The analysis can be aborted from a different thread with + libboardgame_util::set_abort(). + @param game + @param search + @param nu_simulations + @param progress_callback Function that will be called at the beginning + of the analysis of a position. Arguments: number moves analyzed so far, + total number of moves. */ + void run(const Game& game, Search& search, size_t nu_simulations, + function progress_callback); + + Variant get_variant() const; + + unsigned get_nu_moves() const; + + ColorMove get_move(unsigned i) const; + + double get_value(unsigned i) const; + +private: + Variant m_variant; + + vector m_moves; + + vector m_values; +}; + +inline ColorMove AnalyzeGame::get_move(unsigned i) const +{ + LIBBOARDGAME_ASSERT(i < m_moves.size()); + return m_moves[i]; +} + +inline unsigned AnalyzeGame::get_nu_moves() const +{ + return static_cast(m_moves.size()); +} + +inline double AnalyzeGame::get_value(unsigned i) const +{ + LIBBOARDGAME_ASSERT(i < m_values.size()); + return m_values[i]; +} + +inline Variant AnalyzeGame::get_variant() const +{ + return m_variant; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_ANALYZE_GAME_H diff --git a/src/libpentobi_mcts/CMakeLists.txt b/src/libpentobi_mcts/CMakeLists.txt new file mode 100644 index 0000000..9acef96 --- /dev/null +++ b/src/libpentobi_mcts/CMakeLists.txt @@ -0,0 +1,24 @@ +add_library(pentobi_mcts STATIC + AnalyzeGame.h + AnalyzeGame.cpp + Float.h + History.h + History.cpp + Player.h + Player.cpp + PlayoutFeatures.h + PlayoutFeatures.cpp + PriorKnowledge.h + PriorKnowledge.cpp + SearchParamConst.h + SharedConst.h + SharedConst.cpp + Search.h + Search.cpp + State.h + State.cpp + StateUtil.h + StateUtil.cpp + Util.h + Util.cpp +) diff --git a/src/libpentobi_mcts/Float.h b/src/libpentobi_mcts/Float.h new file mode 100644 index 0000000..10ece26 --- /dev/null +++ b/src/libpentobi_mcts/Float.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Float.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_FLOAT_H +#define LIBPENTOBI_MCTS_FLOAT_H + +#include + +namespace libpentobi_mcts { + +//----------------------------------------------------------------------------- + +#ifdef LIBPENTOBI_MCTS_FLOAT_TYPE +typedef LIBPENTOBI_MCTS_FLOAT_TYPE Float; +#else +typedef float Float; +#endif + +static_assert(std::is_floating_point::value, ""); + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_FLOAT_H diff --git a/src/libpentobi_mcts/History.cpp b/src/libpentobi_mcts/History.cpp new file mode 100644 index 0000000..290df35 --- /dev/null +++ b/src/libpentobi_mcts/History.cpp @@ -0,0 +1,77 @@ +//---------------------------------------------------------------------------- +/** @file libpentobi_mcts/History.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//---------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "History.h" + +#include "libpentobi_base/BoardUtil.h" + +namespace libpentobi_mcts { + +using namespace std; +using libpentobi_base::boardutil::get_current_position_as_setup; + +//---------------------------------------------------------------------------- + +void History::get_as_setup(Variant& variant, Setup& setup) const +{ + LIBBOARDGAME_ASSERT(is_valid()); + variant = m_variant; + unique_ptr bd(new Board(variant)); + for (ColorMove mv : m_moves) + bd->play(mv); + get_current_position_as_setup(*bd, setup); +} + +void History::init(const Board& bd, Color to_play) +{ + m_is_valid = true; + m_variant = bd.get_variant(); + m_nu_colors = bd.get_nu_colors(); + m_moves.clear(); + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + m_moves.push_back(bd.get_move(i)); + m_to_play = to_play; +} + +bool History::is_followup( + const History& other, + ArrayList& sequence) const +{ + if (! m_is_valid || ! other.m_is_valid || m_variant != other.m_variant + || m_moves.size() < other.m_moves.size()) + return false; + unsigned i = 0; + for ( ; i < other.m_moves.size(); ++i) + if (m_moves[i] != other.m_moves[i]) + return false; + sequence.clear(); + Color to_play = other.m_to_play; + for ( ; i < m_moves.size(); ++i) + { + auto mv = m_moves[i]; + while (mv.color != to_play) + { + sequence.push_back(Move::null()); + to_play = to_play.get_next(m_nu_colors); + } + sequence.push_back(mv.move); + to_play = to_play.get_next(m_nu_colors); + } + while (m_to_play != to_play) + { + sequence.push_back(Move::null()); + to_play = to_play.get_next(m_nu_colors); + } + return true; +} + +//---------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/History.h b/src/libpentobi_mcts/History.h new file mode 100644 index 0000000..8d9f145 --- /dev/null +++ b/src/libpentobi_mcts/History.h @@ -0,0 +1,105 @@ +//---------------------------------------------------------------------------- +/** @file libpentobi_mcts/History.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//---------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_HISTORY_H +#define LIBPENTOBI_MCTS_HISTORY_H + +#include "SearchParamConst.h" +#include "libpentobi_base/Board.h" + +namespace libpentobi_mcts { + +using libboardgame_util::ArrayList; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::ColorMove; +using libpentobi_base::Move; +using libpentobi_base::Setup; +using libpentobi_base::Variant; + +//---------------------------------------------------------------------------- + +/** Identifier for board state including history. + This class can be used, for instance, to uniquely remember a board + position for reusing parts of previous computations. The state includes: + - the game variant + - the history of moves + - the color to play */ +class History +{ +public: + /** Constructor. + The initial state is that the history does not correspond to any + valid position. */ + History(); + + /** Initialize from a current board position and explicit color to play. */ + void init(const Board& bd, Color to_play); + + /** Clear the state. + A cleared state does not correspond to any valid position. */ + void clear(); + + /** Check if the state corresponds to any valid position. */ + bool is_valid() const; + + /** Check if this position is a alternate-play followup to another one. + @param other The other position + @param[out] sequence The sequence leading from the other position to + this one. Pass (=null) moves are inserted to ensure alternating colors + (as required by libpentobi_mcts::Search.) + @return @c true If the position is a followup + */ + bool is_followup( + const History& other, + ArrayList& sequence) const; + + /** Get the position of the board state as setup. + @pre is_valid() + @param[out] variant + @param[out] setup */ + void get_as_setup(Variant& variant, Setup& setup) const; + + Color get_to_play() const; + +private: + bool m_is_valid; + + Color::IntType m_nu_colors; + + Variant m_variant; + + ArrayList m_moves; + + Color m_to_play; +}; + +inline History::History() +{ + clear(); +} + +inline void History::clear() +{ + m_is_valid = false; +} + +inline Color History::get_to_play() const +{ + LIBBOARDGAME_ASSERT(m_is_valid); + return m_to_play; +} + +inline bool History::is_valid() const +{ + return m_is_valid; +} + +//---------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_HISTORY_H diff --git a/src/libpentobi_mcts/Player.cpp b/src/libpentobi_mcts/Player.cpp new file mode 100644 index 0000000..5bd0a69 --- /dev/null +++ b/src/libpentobi_mcts/Player.cpp @@ -0,0 +1,366 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Player.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Player.h" + +#include +#include +#include "libboardgame_util/CpuTimeSource.h" +#include "libboardgame_util/WallTimeSource.h" +#include "libboardgame_sys/Memory.h" + +namespace libpentobi_mcts { + +using libboardgame_util::CpuTimeSource; +using libboardgame_util::WallTimeSource; +using libpentobi_base::BoardType; + +//----------------------------------------------------------------------------- + +namespace { + +// Rationale for choosing the number of simulations: +// * Level 9, the highest in the desktop version, should be as strong as +// possible on a mid-range PC with reasonable thinking times. The average +// time per game and player is targeted at 2-3 min for the 2-color game +// variants and 5-6 min for the others. +// * Level 7, the highest in the Android version, should be as strong as +// possible on typical mobile hardware. It is set to 4% of level 9. +// * Level 8 is set to 20% of level 9, the middle (on a log scale) between +// level 7 and 9. Since most parameter tuning is done at level 7 or 8, it is +// better for development purposes to define level 8 in terms of time, even +// if it doesn't necessarily correspond to the middle wrt. playing strength. +// * The numbers for level 1 are set to a value that is weak enough for +// beginners without playing silly moves. They are currently chosen depending +// on how strong we estimate Pentobi is in a game variant. It is also taken +// into consideration how much the Elo difference level 1-9 is in self-play +// experiments. After applying the scale factor (see comment in +// Player::get_rating()), we want a range of about 1000 Elo (difference +// between beginner and lower master level). +// * The numbers for level 1-6 are chosen such that they correspond to roughly +// equidistant Elo differences measured in self-play experiments. + +static const float counts_classic[Player::max_supported_level] = + { 3, 18, 75, 311, 1260, 8949, 66179, 330894, 1654470 }; + +static const float counts_duo[Player::max_supported_level] = + { 3, 14, 63, 253, 2203, 13614, 202994, 1014969, 5074843 }; + +static const float counts_trigon[Player::max_supported_level] = + { 228, 376, 733, 1214, 2606, 6802, 18428, 92138, 460691 }; + +static const float counts_nexos[Player::max_supported_level] = + { 250, 347, 625, 1223, 3117, 8270, 22626, 113130, 565651 }; + +static const float counts_callisto_2[Player::max_supported_level] = + { 100, 192, 405, 1079, 3323, 12258, 94104, 470522, 2352609 }; + +} // namespace + +//----------------------------------------------------------------------------- + +Player::Player(Variant initial_variant, unsigned max_level, string books_dir, + unsigned nu_threads) + : m_is_book_loaded(false), + m_use_book(true), + m_resign(false), + m_books_dir(move(books_dir)), + m_max_level(max_level), + m_level(4), + m_fixed_simulations(0), + m_resign_threshold(0.09f), + m_resign_min_simulations(500), + m_search(initial_variant, nu_threads, get_memory()), + m_book(initial_variant), + m_time_source(new WallTimeSource) +{ + for (unsigned i = 0; i < Board::max_player_moves; ++i) + { + // Hand-tuned such that time per move is more evenly spread among all + // moves than with a fixed number of simulations (because the + // simulations per second increase rapidly with the move number) but + // the average time per game is roughly the same. + m_weight_max_count_duo[i] = 0.7f * exp(0.1f * static_cast(i)); + m_weight_max_count_classic[i] = m_weight_max_count_duo[i]; + m_weight_max_count_trigon[i] = m_weight_max_count_duo[i]; + m_weight_max_count_callisto[i] = m_weight_max_count_duo[i]; + m_weight_max_count_callisto_2[i] = m_weight_max_count_duo[i]; + // Less weight for the first move(s) because number of legal moves + // is lower and the search applies some pruning rules to reduce the + // branching factor in early moves + if (i == 0) + { + m_weight_max_count_classic[i] *= 0.2f; + m_weight_max_count_trigon[i] *= 0.2f; + m_weight_max_count_duo[i] *= 0.6f; + m_weight_max_count_callisto[i] *= 0.2f; + m_weight_max_count_callisto_2[i] *= 0.2f; + } + else if (i == 1) + { + m_weight_max_count_classic[i] *= 0.2f; + m_weight_max_count_trigon[i] *= 0.5f; + m_weight_max_count_callisto[i] *= 0.6f; + m_weight_max_count_callisto_2[i] *= 0.2f; + } + else if (i == 2) + { + m_weight_max_count_classic[i] *= 0.3f; + m_weight_max_count_trigon[i] *= 0.6f; + } + else if (i == 3) + { + m_weight_max_count_trigon[i] *= 0.8f; + } + } +} + +Player::~Player() = default; + +Move Player::genmove(const Board& bd, Color c) +{ + m_resign = false; + if (! bd.has_moves(c)) + return Move::null(); + Move mv; + auto variant = bd.get_variant(); + auto board_type = bd.get_board_type(); + auto level = min(max(m_level, 1u), m_max_level); + // Don't use more than 2 moves per color from opening book in lower levels + if (m_use_book + && (level >= 4 || bd.get_nu_moves() < 2u * bd.get_nu_colors())) + { + if (! is_book_loaded(variant)) + load_book(m_books_dir + + "/book_" + to_string_id(variant) + ".blksgf"); + if (m_is_book_loaded) + { + mv = m_book.genmove(bd, c); + if (! mv.is_null()) + return mv; + } + } + Float max_count = 0; + double max_time = 0; + if (m_fixed_simulations > 0) + max_count = m_fixed_simulations; + else if (m_fixed_time > 0) + max_time = m_fixed_time; + else + { + switch (board_type) + { + case BoardType::classic: + max_count = counts_classic[level - 1]; + break; + case BoardType::duo: + max_count = counts_duo[level - 1]; + break; + case BoardType::trigon: + case BoardType::trigon_3: + case BoardType::callisto: + case BoardType::callisto_3: + max_count = counts_trigon[level - 1]; + break; + case BoardType::nexos: + max_count = counts_nexos[level - 1]; + break; + case BoardType::callisto_2: + max_count = counts_callisto_2[level - 1]; + break; + } + // Don't weight max_count in low levels, otherwise it is still too + // strong for beginners (later in the game, the weight becomes much + // greater than 1 because the simulations become very fast) + bool weight_max_count = (level >= 4); + if (weight_max_count) + { + auto player_move = bd.get_nu_onboard_pieces(c); + float weight = 1; // Init to avoid compiler warning + switch (board_type) + { + case BoardType::classic: + weight = m_weight_max_count_classic[player_move]; + break; + case BoardType::duo: + weight = m_weight_max_count_duo[player_move]; + break; + case BoardType::callisto: + case BoardType::callisto_3: + weight = m_weight_max_count_callisto[player_move]; + break; + case BoardType::callisto_2: + weight = m_weight_max_count_callisto_2[player_move]; + break; + case BoardType::trigon: + case BoardType::trigon_3: + case BoardType::nexos: + weight = m_weight_max_count_trigon[player_move]; + break; + } + max_count = ceil(max_count * weight); + } + } + if (max_count != 0) + LIBBOARDGAME_LOG("MaxCnt ", fixed, setprecision(0), max_count); + else + LIBBOARDGAME_LOG("MaxTime ", max_time); + if (! m_search.search(mv, bd, c, max_count, 0, max_time, *m_time_source)) + return Move::null(); + // Resign only in two-player game variants + if (get_nu_players(variant) == 2) + if (m_search.get_root_visit_count() > m_resign_min_simulations + && m_search.get_root_val().get_mean() < m_resign_threshold) + m_resign = true; + return mv; +} + +/** Suggest how much memory to use for the trees depending on the maximum + level used. */ +size_t Player::get_memory() +{ + size_t available = libboardgame_sys::get_memory(); + if (available == 0) + { + LIBBOARDGAME_LOG("WARNING: could not determine system memory" + " (assuming 512MB)"); + available = 512000000; + } + // Don't use all of the available memory +#if PENTOBI_LOW_RESOURCES + size_t reasonable = available / 4; +#else + size_t reasonable = available / 3; +#endif + size_t wanted = 2000000000; + if (m_max_level < max_supported_level) + { + // We don't need so much memory if m_max_level is smaller than + // max_supported_level. Trigon has the highest relative number of + // simulations on lower levels compared to the highest level. The + // memory used in a search is not proportional to the number of + // simulations (e.g. because the expand threshold increases with the + // depth). We approximate this by adding an exponent to the ratio + // and not taking into account if m_max_level is very small. + LIBBOARDGAME_ASSERT(max_supported_level >= 5); + auto factor = pow(counts_trigon[max_supported_level - 1] + / counts_trigon[max(m_max_level, 5u) - 1], 0.8); + wanted = static_cast(double(wanted) / factor); + } + size_t memory = min(wanted, reasonable); + LIBBOARDGAME_LOG("Using ", memory / 1000000, " MB of ", + available / 1000000, " MB"); + return memory; +} + +Rating Player::get_rating(Variant variant, unsigned level) +{ + // The ratings are roughly based on Elo differences measured in self-play + // experiments. The measured values are scaled with a factor smaller than 1 + // to take into account that self-play usually overestimates the strength + // against humans. The anchor is set to about 1000 (beginner level) for + // level 1. The exact value for anchor and scale is chosen according to our + // estimate how strong Pentobi plays at level 1 and level 9 in each game + // variant (2000 Elo would be lower expert level). Currently, only 2-player + // variants are tested and the ratings are used for multi-player variants + // on the same board. + auto max_supported_level = Player::max_supported_level; + level = min(max(level, 1u), max_supported_level); + Rating result; + switch (get_board_type(variant)) + { + case BoardType::classic: + { + // Anchor 1000, scale 0.63 + static float elo[Player::max_supported_level] = + { 1000, 1145, 1290, 1435, 1580, 1725, 1870, 1957, 2021 }; + result = Rating(elo[level - 1]); + } + break; + case BoardType::duo: + { + // Anchor 1100, scale 0.7 + static float elo[Player::max_supported_level] = + { 1100, 1269, 1438, 1607, 1776, 1945, 2114, 2165, 2209 }; + result = Rating(elo[level - 1]); + } + break; + case BoardType::callisto_2: + { + // Anchor 1000, scale 0.63 + static float elo[Player::max_supported_level] = + { 1000, 1101, 1203, 1304, 1405, 1507, 1608, 1673, 1756 }; + result = Rating(elo[level - 1]); + } + break; + case BoardType::trigon: + case BoardType::trigon_3: + { + // Anchor 1000, scale 0.60 + static float elo[Player::max_supported_level] = + { 1000, 1103, 1206, 1308, 1411, 1514, 1617, 1757, 1856 }; + result = Rating(elo[level - 1]); + } + break; + case BoardType::nexos: + case BoardType::callisto: // Not measured + case BoardType::callisto_3: // Not measured + { + // Anchor 1000, scale 0.60 + static float elo[Player::max_supported_level] = + { 1000, 1101, 1202, 1304, 1406, 1507, 1608, 1698, 1799 }; + result = Rating(elo[level - 1]); + } + break; + } + return result; +} + +bool Player::is_book_loaded(Variant variant) const +{ + return m_is_book_loaded && m_book.get_tree().get_variant() == variant; +} + +void Player::load_book(istream& in) +{ + m_book.load(in); + m_is_book_loaded = true; +} + +bool Player::load_book(const string& filepath) +{ + ifstream in(filepath); + if (! in) + { + LIBBOARDGAME_LOG("Could not load book ", filepath); + return false; + } + m_book.load(in); + m_is_book_loaded = true; + LIBBOARDGAME_LOG("Loaded book ", filepath); + return true; +} + +bool Player::resign() const +{ + return m_resign; +} + +void Player::use_cpu_time(bool enable) +{ + if (enable) + m_time_source.reset(new CpuTimeSource); + else + m_time_source.reset(new WallTimeSource); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/Player.h b/src/libpentobi_mcts/Player.h new file mode 100644 index 0000000..3aeb8ce --- /dev/null +++ b/src/libpentobi_mcts/Player.h @@ -0,0 +1,193 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Player.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_PLAYER_H +#define LIBPENTOBI_MCTS_PLAYER_H + +#include "Search.h" +#include "libboardgame_base/Rating.h" +#include "libpentobi_base/Book.h" +#include "libpentobi_base/PlayerBase.h" + +namespace libpentobi_mcts { + +using libboardgame_base::Rating; +using libpentobi_base::Book; +using libpentobi_base::PlayerBase; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class Player final + : public PlayerBase +{ +public: + static const unsigned max_supported_level = 9; + + /** Constructor. + @param initial_variant Game variant to initialize the internal + board with (may avoid unnecessary BoardConst creation for game variant + that is never used) + @param max_level The maximum level used + @param books_dir Directory containing opening books. + @param nu_threads The number of threads to use in the search (0 means + to select a reasonable default value) */ + Player(Variant initial_variant, unsigned max_level, string books_dir, unsigned nu_threads = 0); + + ~Player(); + + Move genmove(const Board& bd, Color c) override; + + bool resign() const override; + + Float get_fixed_simulations() const; + + double get_fixed_time() const; + + /** Use a fixed number of simulations in the search. + If set to a value greater than zero, this value will enforce a + fixed number of simulations per search independent of the playing + level. */ + void set_fixed_simulations(Float n); + + /** Use a fixed time limit per move. + If set to a value greater than zero, this value will set a fixed + (maximum) time per search independent of the playing level. */ + void set_fixed_time(double seconds); + + bool get_use_book() const; + + void set_use_book(bool enable); + + unsigned get_level() const; + + void set_level(unsigned level); + + /** Use CPU time instead of Wall time to measure time. */ + void use_cpu_time(bool enable); + + Search& get_search(); + + void load_book(istream& in); + + /** Is a book loaded and compatible with a given game variant? */ + bool is_book_loaded(Variant variant) const; + + /** Get an estimated Elo-rating of a level. + This rating is an estimated rating when playing vs. humans. Although + it is based on computer vs. computer experiments, the ratings were + modified and rescaled to take into account that self-play experiments + usually overestimate the rating differences when playing against + humans. */ + static Rating get_rating(Variant variant, unsigned level); + + /** Get an estimated Elo-rating of the current level. */ + Rating get_rating(Variant variant) const; + +private: + bool m_is_book_loaded; + + bool m_use_book; + + bool m_resign; + + string m_books_dir; + + unsigned m_max_level; + + unsigned m_level; + + array m_weight_max_count_classic; + + array m_weight_max_count_trigon; + + array m_weight_max_count_duo; + + array m_weight_max_count_callisto; + + array m_weight_max_count_callisto_2; + + Float m_fixed_simulations; + + Float m_resign_threshold; + + Float m_resign_min_simulations; + + double m_fixed_time; + + Search m_search; + + Book m_book; + + unique_ptr m_time_source; + + + size_t get_memory(); + + void init_settings(); + + bool load_book(const string& filepath); +}; + +inline Float Player::get_fixed_simulations() const +{ + return m_fixed_simulations; +} + +inline double Player::get_fixed_time() const +{ + return m_fixed_time; +} + +inline unsigned Player::get_level() const +{ + return m_level; +} + +inline Rating Player::get_rating(Variant variant) const +{ + return get_rating(variant, m_level); +} + +inline Search& Player::get_search() +{ + return m_search; +} + +inline bool Player::get_use_book() const +{ + return m_use_book; +} + +inline void Player::set_fixed_simulations(Float n) +{ + m_fixed_simulations = n; + m_fixed_time = 0; +} + +inline void Player::set_fixed_time(double seconds) +{ + m_fixed_time = seconds; + m_fixed_simulations = 0; +} + +inline void Player::set_level(unsigned level) +{ + m_level = level; + m_fixed_simulations = 0; + m_fixed_time = 0; +} + +inline void Player::set_use_book(bool enable) +{ + m_use_book = enable; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_PLAYER_H diff --git a/src/libpentobi_mcts/PlayoutFeatures.cpp b/src/libpentobi_mcts/PlayoutFeatures.cpp new file mode 100644 index 0000000..54fe564 --- /dev/null +++ b/src/libpentobi_mcts/PlayoutFeatures.cpp @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/PlayoutFeatures.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PlayoutFeatures.h" + +namespace libpentobi_mcts { + +//----------------------------------------------------------------------------- + +PlayoutFeatures::PlayoutFeatures() = default; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/PlayoutFeatures.h b/src/libpentobi_mcts/PlayoutFeatures.h new file mode 100644 index 0000000..a3a535f --- /dev/null +++ b/src/libpentobi_mcts/PlayoutFeatures.h @@ -0,0 +1,215 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/PlayoutFeatures.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_PLAYOUT_FEATURES_H +#define LIBPENTOBI_MCTS_PLAYOUT_FEATURES_H + +#include "libpentobi_base/Board.h" +#include "libpentobi_base/PointList.h" + +namespace libpentobi_mcts { + +using namespace std; +using libboardgame_base::ArrayList; +using libboardgame_util::Range; +using libpentobi_base::Board; +using libpentobi_base::BoardConst; +using libpentobi_base::Color; +using libpentobi_base::ColorMove; +using libpentobi_base::Geometry; +using libpentobi_base::Grid; +using libpentobi_base::GridExt; +using libpentobi_base::Move; +using libpentobi_base::MoveInfo; +using libpentobi_base::MoveInfoExt; +using libpentobi_base::PieceInfo; +using libpentobi_base::Point; +using libpentobi_base::PointList; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** Compute move features for the playout policy. + This class encodes features that correspond to points on the board in bit + ranges of an integer, such that the sum of the features values for all + points of a move can be quickly computed in the playout move generation. + Currently, there are only two features: the forbidden status and whether + the point is a local point. Local points are attach points of recent + opponent moves or points that are adjacent to them. Local points that + are attach points of the color to play count double. + During a simulation, some of the features are updated incrementally + (forbidden status) and some non-incrementally (local points). */ +class PlayoutFeatures +{ +public: + typedef unsigned IntType; + + /** The maximum number of local points for a move. + The number can be higher than PieceInfo::max_size (see class + description). */ + static const unsigned max_local = 2 * PieceInfo::max_size; + + /** Compute the sum of the feature values for a move. */ + class Compute + { + public: + /** Constructor. + @param p The first point of the move + @param playout_features */ + Compute(Point p, const PlayoutFeatures& playout_features) + : m_value(playout_features.m_point_value[p]) + { } + + /** Add a point of the move. */ + void add(Point p, const PlayoutFeatures& playout_features) + { + m_value += playout_features.m_point_value[p]; + } + + bool is_forbidden() const + { + return (m_value & 0xf000u) != 0; + } + + /** Get the number of local points for this move. + @pre ! is_forbidden() + @return The number of local points in [0..max_local] */ + IntType get_nu_local() const + { + LIBBOARDGAME_ASSERT(! is_forbidden()); + return m_value; + } + + private: + IntType m_value; + }; + + friend class Compute; + + PlayoutFeatures(); + + /** Initialize snapshot with forbidden state. */ + void init_snapshot(const Board& bd, Color c); + + void restore_snapshot(const Board& bd); + + /** Set points of move to forbidden. */ + template + void set_forbidden(const MoveInfo& info); + + /** Set adjacent points of move to forbidden. */ + template + void set_forbidden(const MoveInfoExt& info_ext); + + template + void set_local(const Board& bd); + +private: + GridExt m_point_value; + + Grid m_snapshot; + + /** Points with non-zero local value. */ + PointList m_local_points; +}; + +inline void PlayoutFeatures::init_snapshot(const Board& bd, Color c) +{ + m_point_value[Point::null()] = 0; + auto& is_forbidden = bd.is_forbidden(c); + for (Point p : bd) + m_snapshot[p] = (is_forbidden[p] ? 0x1000u : 0); +} + + +inline void PlayoutFeatures::restore_snapshot(const Board& bd) +{ + m_point_value.copy_from(m_snapshot, bd.get_geometry()); +} + +template +inline void PlayoutFeatures::set_forbidden(const MoveInfo& info) +{ + auto p = info.begin(); + for (unsigned i = 0; i < MAX_SIZE; ++i, ++p) + m_point_value[*p] = 0x1000u; + m_point_value[Point::null()] = 0; +} + +template +inline void PlayoutFeatures::set_forbidden( + const MoveInfoExt& info_ext) +{ + for (auto i = info_ext.begin_adj(), end = info_ext.end_adj(); i != end; + ++i) + m_point_value[*i] = 0x1000u; +} + +template +inline void PlayoutFeatures::set_local(const Board& bd) +{ + // Clear old info about local points + for (Point p : m_local_points) + m_point_value[p] &= 0xf000u; + unsigned nu_local = 0; + + Color to_play = bd.get_to_play(); + Color second_color; + if (bd.get_variant() == Variant::classic_3 && to_play.to_int() == 3) + second_color = Color(bd.get_alt_player()); + else + second_color = bd.get_second_color(to_play); + auto& geo = bd.get_geometry(); + auto& moves = bd.get_moves(); + auto move_info_ext_array = bd.get_board_const().get_move_info_ext_array(); + // Consider last 3 moves for local points (i.e. last 2 opponent moves in + // two-color variants) + auto end = moves.end(); + auto begin = (end - moves.begin() < 3 ? moves.begin() : end - 3); + for (auto i = begin; i != end; ++i) + { + Color c = i->color; + if (c == to_play || c == second_color) + continue; + Move mv = i->move; + auto& is_forbidden = bd.is_forbidden(c); + auto& info_ext = BoardConst::get_move_info_ext( + mv, move_info_ext_array); + auto j = info_ext.begin_attach(); + auto end = info_ext.end_attach(); + do + { + if (is_forbidden[*j]) + continue; + if (m_point_value[*j] == 0) + { + m_local_points.get_unchecked(nu_local++) = *j; + m_point_value[*j] = + 1 + static_cast( + bd.is_attach_point(*j, to_play)); + } + if (MAX_SIZE == 7) // Nexos + LIBBOARDGAME_ASSERT(geo.get_adj(*j).empty()); + else + for (Point k : geo.get_adj(*j)) + if (! is_forbidden[k] && m_point_value[k] == 0) + { + m_local_points.get_unchecked(nu_local++) = k; + m_point_value[k] = + 1 + static_cast( + bd.is_attach_point(k, to_play)); + } + } + while (++j != end); + } + m_local_points.resize(nu_local); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_PLAYOUT_FEATURES_H diff --git a/src/libpentobi_mcts/PriorKnowledge.cpp b/src/libpentobi_mcts/PriorKnowledge.cpp new file mode 100644 index 0000000..369347e --- /dev/null +++ b/src/libpentobi_mcts/PriorKnowledge.cpp @@ -0,0 +1,132 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/PriorKnowledge.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PriorKnowledge.h" + +#include +#include "libboardgame_util/MathUtil.h" + +namespace libpentobi_mcts { + +using libboardgame_util::fast_exp; +using libpentobi_base::BoardType; +using libpentobi_base::Color; +using libpentobi_base::PointState; +using libpentobi_base::PieceInfo; +using libpentobi_base::PieceSet; + +//----------------------------------------------------------------------------- + +PriorKnowledge::PriorKnowledge() +{ + m_is_local.fill_all(false); +} + +void PriorKnowledge::start_search(const Board& bd) +{ + auto& geo = bd.get_geometry(); + auto board_type = bd.get_board_type(); + auto piece_set = bd.get_piece_set(); + + // Init m_dist_to_center + float width = static_cast(geo.get_width()); + float height = static_cast(geo.get_height()); + float center_x = 0.5f * width - 0.5f; + float center_y = 0.5f * height - 0.5f; + bool is_trigon = (piece_set == PieceSet::trigon); + float ratio = (is_trigon ? 1.732f : 1); + for (Point p : geo) + { + float x = static_cast(geo.get_x(p)); + float y = static_cast(geo.get_y(p)); + float dx = x - center_x; + float dy = ratio * (y - center_y); + float d = sqrt(dx * dx + dy * dy); + if (board_type == BoardType::classic) + // Don't make a distinction between moves close enough to the + // center in game variant Classic/Classic2 + d = max(d, 2.f); + m_dist_to_center[p] = d; + } + m_dist_to_center[Point::null()] = numeric_limits::max(); + + // Init m_check_dist_to_center + switch(bd.get_variant()) + { + case Variant::classic: + case Variant::classic_2: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 12; + m_max_dist_diff = 0.3f; + break; + case Variant::classic_3: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 10; + m_max_dist_diff = 0.3f; + break; + case Variant::trigon: + case Variant::trigon_2: + case Variant::trigon_3: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 3; + m_max_dist_diff = 0.5f; + break; + case Variant::duo: + case Variant::junior: + m_check_dist_to_center.fill(false); + break; + case Variant::callisto: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 4; + m_max_dist_diff = 0; + break; + case Variant::callisto_2: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 4; + m_max_dist_diff = 0; + break; + case Variant::callisto_3: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 3; + m_max_dist_diff = 0; + break; + case Variant::nexos: + case Variant::nexos_2: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 7; + m_max_dist_diff = 0.3f; + break; + } + + if (piece_set != PieceSet::callisto) + // Don't check dist to center if the position was setup in a way that + // placed pieces but did not cover the starting point(s), otherwise the + // search might not generate any moves (if no moves meet the + // dist-to-center condition). Even if such positions cannot occur in + // legal games, we still don't want the move generation to fail. + for (Color c : bd.get_colors()) + { + if (bd.get_nu_onboard_pieces(c) == 0) + continue; + bool is_starting_point_covered = false; + for (Point p : bd.get_starting_points(c)) + if (bd.get_point_state(p) == PointState(c)) + { + is_starting_point_covered = true; + break; + } + if (! is_starting_point_covered) + m_check_dist_to_center[c] = false; + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/PriorKnowledge.h b/src/libpentobi_mcts/PriorKnowledge.h new file mode 100644 index 0000000..6bfae56 --- /dev/null +++ b/src/libpentobi_mcts/PriorKnowledge.h @@ -0,0 +1,432 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/PriorKnowledge.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_PRIOR_KNOWLEDGE_H +#define LIBPENTOBI_MCTS_PRIOR_KNOWLEDGE_H + +#include "Float.h" +#include "SearchParamConst.h" +#include "libboardgame_mcts/Tree.h" +#include "libboardgame_util/MathUtil.h" +#include "libpentobi_base/Board.h" + +namespace libpentobi_mcts { + +using namespace std; +using libboardgame_util::fast_exp; +using libpentobi_base::Board; +using libpentobi_base::BoardConst; +using libpentobi_base::ColorMap; +using libpentobi_base::ColorMove; +using libpentobi_base::Grid; +using libpentobi_base::GridExt; +using libpentobi_base::Move; +using libpentobi_base::MoveList; +using libpentobi_base::Point; +using libpentobi_base::PointList; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** Initializes newly created nodes with heuristic prior count and value. */ +class PriorKnowledge +{ +public: + typedef libboardgame_mcts::Node + Node; + + typedef libboardgame_mcts::Tree Tree; + + PriorKnowledge(); + + void start_search(const Board& bd); + + /** Generate children nodes initialized with prior knowledge. + @return false If the tree has not enough capacity for the children. */ + template + bool gen_children(const Board& bd, const MoveList& moves, + bool is_symmetry_broken, Tree::NodeExpander& expander, + Float root_val); + +private: + struct MoveFeatures + { + /** Heuristic value of the move expressed in score points. */ + Float heuristic; + + bool is_local; + + /** Does the move touch a piece of the same player? */ + bool connect; + + /** Only used on Classic and Trigon boards. */ + float dist_to_center; + }; + + + array m_features; + + /** Maximum of Features::heuristic for all moves. */ + Float m_max_heuristic; + + bool m_has_connect_move; + + ColorMap m_check_dist_to_center; + + unsigned m_dist_to_center_max_pieces; + + float m_min_dist_to_center; + + float m_max_dist_diff; + + /** Marker for attach points of recent opponent moves. */ + GridExt m_is_local; + + /** Points in m_is_local with value greater zero. */ + PointList m_local_points; + + /** Distance to center heuristic. */ + GridExt m_dist_to_center; + + + template + void compute_features(const Board& bd, const MoveList& moves, + bool check_dist_to_center, bool check_connect); + + template + void init_local(const Board& bd); +}; + + +template +void PriorKnowledge::compute_features(const Board& bd, const MoveList& moves, + bool check_dist_to_center, + bool check_connect) +{ + auto to_play = bd.get_to_play(); + auto variant = bd.get_variant(); + Color second_color; + // connect_color is the 2nd color of the player in game variants with 2 + // colors per player (connecting to_play and connect_color is good) and + // to_play in other game variants (which disables the bonus without + // needing an extra check below because adj_point_value is not used for + // pieces of to_play because it is illegal for to_play to play there). + Color connect_color; + if (variant == Variant::classic_3 && to_play.to_int() == 3) + { + second_color = Color(bd.get_alt_player()); + connect_color = to_play; + } + else + { + second_color = bd.get_second_color(to_play); + connect_color = second_color; + } + auto& bc = bd.get_board_const(); + auto& geo = bc.get_geometry(); + auto move_info_array = bc.get_move_info_array(); + auto move_info_ext_array = bc.get_move_info_ext_array(); + auto& is_forbidden = bd.is_forbidden(to_play); + GridExt point_value; + point_value[Point::null()] = 0; + Grid attach_point_value; + Grid adj_point_value; + for (Point p : geo) + { + auto s = bd.get_point_state(p); + if (is_forbidden[p]) + { + if (s != to_play) + attach_point_value[p] = -2.5; + else + attach_point_value[p] = 0.5; + if (s == connect_color) + // Connecting own colors is good + adj_point_value[p] = 1; + else if (! s.is_empty()) + // Touching opponent is better than playing elsewhere (no need to + // check if s == to_play, such moves are illegal) + adj_point_value[p] = 0.4f; + else + adj_point_value[p] = 0; + } + else + { + point_value[p] = 1; + attach_point_value[p] = 0.5; + if (bd.is_attach_point(p, to_play)) + // Making own attach point forbidden is especially bad + adj_point_value[p] = -1; + else + // Creating new forbidden points is a bad thing + adj_point_value[p] = -0.1f; + } + } + for (Color c : bd.get_colors()) + { + if (c == to_play || c == second_color) + continue; + auto& is_forbidden = bd.is_forbidden(c); + for (Point p : bd.get_attach_points(c)) + if (! is_forbidden[p]) + { + // Occupying opponent attach points or points adjacent to them + // is very good + point_value[p] = 3.f; + for (Point j : geo.get_adj(p)) + if (! is_forbidden[j]) + point_value[j] = 3.f; + } + } + if (variant == Variant::classic_2 + || (variant == Variant::classic_3 && second_color != to_play)) + { + auto& is_forbidden_second_color = bd.is_forbidden(second_color); + for (Point p : bd.get_attach_points(second_color)) + if (! is_forbidden_second_color[p]) + { + // Occupying attach points of second color is bad + point_value[p] -= 3.f; + if (! is_forbidden[p]) + // Sharing an attach point with second color is bad + attach_point_value[p] -= 1.f; + } + } + m_max_heuristic = -numeric_limits::max(); + m_min_dist_to_center = numeric_limits::max(); + m_has_connect_move = false; + for (unsigned i = 0; i < moves.size(); ++i) + { + auto mv = moves[i]; + auto info = BoardConst::get_move_info(mv, move_info_array); + auto& info_ext = BoardConst::get_move_info_ext( + mv, move_info_ext_array); + auto& features = m_features[i]; + auto j = info.begin(); + Float heuristic = point_value[*j]; + bool local = m_is_local[*j]; + if (! check_dist_to_center) + for (unsigned k = 1; k < MAX_SIZE; ++k) + { + ++j; + heuristic += point_value[*j]; + // Logically, we mean: local = local || m_is_local[*j] + // But this generates branches, which are bad for performance + // in this tight loop (unrolled by the compiler). So we use a + // bitwise OR, which works because C++ guarantees that + // true/false converts to 1/0. + local |= m_is_local[*j]; + } + else + { + features.dist_to_center = m_dist_to_center[*j]; + for (unsigned k = 1; k < MAX_SIZE; ++k) + { + ++j; + heuristic += point_value[*j]; + // See comment above about bitwise OR on bool + local |= m_is_local[*j]; + features.dist_to_center = + min(features.dist_to_center, m_dist_to_center[*j]); + } + m_min_dist_to_center = + min(m_min_dist_to_center, features.dist_to_center); + } + j = info_ext.begin_attach(); + auto end = info_ext.end_attach(); + heuristic += attach_point_value[*j]; + while (++j != end) + heuristic += attach_point_value[*j]; + if (MAX_SIZE == 7) // Nexos + { + LIBBOARDGAME_ASSERT(info_ext.size_adj_points == 0); + LIBBOARDGAME_ASSERT(! check_connect); + } + else + { + j = info_ext.begin_adj(); + end = info_ext.end_adj(); + if (! check_connect) + { + for ( ; j != end; ++j) + heuristic += adj_point_value[*j]; + } + else + { + features.connect = (bd.get_point_state(*j) == second_color); + for ( ; j != end; ++j) + { + heuristic += adj_point_value[*j]; + if (bd.get_point_state(*j) == second_color) + features.connect = true; + } + if (features.connect) + m_has_connect_move = true; + } + } + if (heuristic > m_max_heuristic) + m_max_heuristic = heuristic; + features.heuristic = heuristic; + features.is_local = local; + } +} + +template +bool PriorKnowledge::gen_children(const Board& bd, const MoveList& moves, + bool is_symmetry_broken, + Tree::NodeExpander& expander, Float root_val) +{ + if (moves.empty()) + { + // Add a pass move. The initialization value does not matter for a + // single child, but we need to use SearchParamConst::child_min_count + // for the count to avoid an assertion. + if (! expander.check_capacity(1)) + return false; + expander.add_child(Move::null(), root_val, 3); + return true; + } + init_local(bd); + auto to_play = bd.get_to_play(); + auto nu_onboard_pieces = bd.get_nu_onboard_pieces(); + bool check_dist_to_center = + (m_check_dist_to_center[to_play] + && nu_onboard_pieces <= m_dist_to_center_max_pieces); + bool check_connect = + (bd.get_variant() == Variant::classic_2 && nu_onboard_pieces < 14); + compute_features(bd, moves, check_dist_to_center, + check_connect); + if (! m_has_connect_move) + check_connect = false; + Move symmetric_mv = Move::null(); + bool has_symmetry_breaker = false; + if (! is_symmetry_broken) + { + unsigned nu_moves = bd.get_nu_moves(); + if (to_play == Color(1) || to_play == Color(3)) + { + if (nu_moves > 0) + { + ColorMove last = bd.get_move(nu_moves - 1); + symmetric_mv = + bd.get_move_info_ext_2(last.move).symmetric_move; + } + } + else if (nu_moves > 0) + for (Move mv : moves) + if (bd.get_move_info_ext_2(mv).breaks_symmetry) + { + has_symmetry_breaker = true; + break; + } + } + m_min_dist_to_center += m_max_dist_diff; + if (! expander.check_capacity(static_cast(moves.size()))) + return false; + for (unsigned i = 0; i < moves.size(); ++i) + { + const auto& features = m_features[i]; + + // Depending on the game variant, prune early moves that don't minimize + // dist to center and moves that don't connect in the middle if + // connection is possible + if ((check_dist_to_center + && features.dist_to_center > m_min_dist_to_center) + || (check_connect && ! features.connect)) + continue; + + auto mv = moves[i]; + + // Convert the heuristic, which is so far estimated in score points, + // into a win/loss value in [0..1] by making it relative to the + // heuristic of the best move and let it decrease exponentially with a + // certain width. We could use exp(-c*x) here, but we use + // 0.1+0.9*exp(-c*x) instead to avoid that the value is too close to + // 0, because then it might never get explored in practice if the bias + // term constant is small. + Float heuristic = m_max_heuristic - features.heuristic; + heuristic = 0.1f + 0.9f * fast_exp(-0.6f * heuristic); + + // Initialize value from heuristic and root_val, each with a count + // of 1.5. If this is changed, SearchParamConst::child_min_count + // should be updated. + Float value = 1.5f * (heuristic + root_val); + Float count = 3; + + // If a symmetric draw is still possible, encourage exploring a move + // that keeps or breaks the symmetry by adding 5 wins or 5 losses + // See also the comment in evaluate_playout() + if (! symmetric_mv.is_null()) + { + if (mv == symmetric_mv) + value += 5; + count += 5; + } + else if (has_symmetry_breaker + && ! bd.get_move_info_ext_2(mv).breaks_symmetry) + continue; + + // Add 1 win for moves that are local responses to recent opponent + // moves + if (features.is_local) + { + value += 1; + count += 1; + } + + LIBBOARDGAME_ASSERT(bd.is_legal(to_play, mv)); + expander.add_child(mv, value / count, count); + } + return true; +} + +template +inline void PriorKnowledge::init_local(const Board& bd) +{ + for (Point p : m_local_points) + m_is_local[p] = false; + unsigned nu_local = 0; + Color to_play = bd.get_to_play(); + Color second_color; + if (bd.get_variant() == Variant::classic_3 && to_play.to_int() == 3) + second_color = Color(bd.get_alt_player()); + else + second_color = bd.get_second_color(to_play); + auto& moves = bd.get_moves(); + auto move_info_ext_array = bd.get_board_const().get_move_info_ext_array(); + // Consider last 3 moves for local points (i.e. last 2 opponent moves in + // two-color variants) + auto end = moves.end(); + auto begin = (end - moves.begin() < 3 ? moves.begin() : end - 3); + for (auto i = begin; i != end; ++i) + { + Color c = i->color; + if (c == to_play || c == second_color) + continue; + auto mv = i->move; + auto& is_forbidden = bd.is_forbidden(c); + auto& info_ext = BoardConst::get_move_info_ext( + mv, move_info_ext_array); + auto j = info_ext.begin_attach(); + auto end = info_ext.end_attach(); + do + { + if (is_forbidden[*j]) + continue; + if (! m_is_local[*j]) + m_local_points.get_unchecked(nu_local++) = *j; + m_is_local[*j] = true; + } + while (++j != end); + } + m_local_points.resize(nu_local); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_PRIOR_KNOWLEDGE_H diff --git a/src/libpentobi_mcts/Search.cpp b/src/libpentobi_mcts/Search.cpp new file mode 100644 index 0000000..26e1e88 --- /dev/null +++ b/src/libpentobi_mcts/Search.cpp @@ -0,0 +1,171 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Search.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Search.h" + +#include "Util.h" + +namespace libpentobi_mcts { + +//----------------------------------------------------------------------------- + +Search::Search(Variant initial_variant, unsigned nu_threads, size_t memory) + : SearchBase(nu_threads == 0 ? util::get_nu_threads() : nu_threads, + memory), + m_auto_param(true), + m_variant(initial_variant), + m_shared_const(m_to_play) +{ + set_default_param(m_variant); + create_threads(); +} + +Search::~Search() = default; + +bool Search::check_followup(ArrayList& sequence) +{ + auto& bd = get_board(); + m_history.init(bd, m_to_play); + bool is_followup = m_history.is_followup(m_last_history, sequence); + + // If avoid_symmetric_draw is enabled, class State uses a different + // evaluation function depending on which player is to play in the root + // position (the first player knows about symmetric draws to be able to + // play a symmetry breaker but the second player pretends not to know about + // symmetric draws to avoid going for such a draw). In this case, we cannot + // reuse parts of the old search tree if the computer plays both colors. + if (m_shared_const.avoid_symmetric_draw + && is_followup && m_to_play != m_last_history.get_to_play() + && has_central_symmetry(bd.get_variant()) + && ! check_symmetry_broken(bd)) + is_followup = false; + + m_last_history = m_history; + return is_followup; +} + +unique_ptr Search::create_state() +{ + return unique_ptr(new State(m_variant, m_shared_const)); +} + +void Search::get_root_position(Variant& variant, Setup& setup) const +{ + m_last_history.get_as_setup(variant, setup); + setup.to_play = m_to_play; +} + +void Search::on_start_search(bool is_followup) +{ + m_shared_const.init(is_followup); +} + +bool Search::search(Move& mv, const Board& bd, Color to_play, + Float max_count, size_t min_simulations, + double max_time, TimeSource& time_source) +{ + m_shared_const.board = &bd; + m_to_play = to_play; + auto variant = bd.get_variant(); + if (m_auto_param && variant != m_variant) + set_default_param(variant); + m_variant = variant; + bool result = SearchBase::search(mv, max_count, min_simulations, max_time, + time_source); + // Search doesn't generate all useless one-piece moves in Callisto + if (result && mv.is_null() && bd.get_piece_set() == PieceSet::callisto + && bd.is_piece_left(to_play, bd.get_one_piece())) + { + for (Point p : bd) + if (! bd.is_forbidden(p, to_play) && ! bd.is_center_section(p)) + { + auto moves = bd.get_board_const().get_moves(bd.get_one_piece(), + p, 0); + LIBBOARDGAME_ASSERT(moves.size() == 1); + mv = *moves.begin(); + result = true; + break; + } + } + return result; +} + +void Search::set_default_param(Variant variant) +{ + LIBBOARDGAME_LOG("Setting default parameters for ", to_string(variant)); + set_expand_threshold(1); + set_expand_threshold_inc(0.5f); + set_rave_weight(0.7f); + set_rave_child_max(2000); + // The following parameters are currently tuned for duo, classic_2 and + // trigon_2 and used for all other game variants with the same board type + switch (variant) + { + case Variant::classic: + case Variant::classic_2: + case Variant::classic_3: + set_exploration_constant(0.021f); + set_rave_parent_max(50000); + break; + case Variant::duo: + case Variant::junior: + set_exploration_constant(0.020f); + set_rave_parent_max(25000); + break; + case Variant::trigon: + case Variant::trigon_2: + case Variant::trigon_3: + case Variant::callisto: + case Variant::callisto_3: + set_exploration_constant(0.014f); + set_rave_parent_max(50000); + break; + case Variant::nexos: + case Variant::nexos_2: + set_exploration_constant(0.008f); + set_rave_parent_max(50000); + break; + case Variant::callisto_2: + set_exploration_constant(0.011f); + set_rave_parent_max(25000); + break; + } +} + +string Search::get_info() const +{ + if (get_nu_simulations() == 0) + return string(); + auto& root = get_tree().get_root(); + if (! root.has_children()) + return string(); + ostringstream s; + s << SearchBase::get_info() + << "Mov: " << root.get_nu_children() << ", "; + if (libpentobi_base::get_nu_players(m_variant) > 2) + { + s << "All:"; + for (PlayerInt i = 0; i < libpentobi_base::get_nu_colors(m_variant); + ++i) + { + if (get_root_val(i).get_count() == 0) + s << " -"; + else + s << " " << setprecision(2) << get_root_val(i).get_mean(); + } + s << ", "; + } + s << get_state(0).get_info(); + return s.str(); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/Search.h b/src/libpentobi_mcts/Search.h new file mode 100644 index 0000000..e1d41cb --- /dev/null +++ b/src/libpentobi_mcts/Search.h @@ -0,0 +1,162 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Search.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_SEARCH_H +#define LIBPENTOBI_MCTS_SEARCH_H + +#include "History.h" +#include "SearchParamConst.h" +#include "State.h" +#include "libboardgame_mcts/SearchBase.h" + +namespace libpentobi_mcts { + +using namespace std; +using libboardgame_mcts::PlayerInt; +using libboardgame_util::TimeSource; +using libpentobi_base::Setup; + +//----------------------------------------------------------------------------- + +/** Monte-Carlo tree search implementation for Blokus. + Multiple colors per player (e.g. in Classic 2) are handled by using the + same game result for each color of a player. + Multiple players of a color (the 4th color in Classic 3) are handled by + adding additional players for each player of this color that share the + game result with the main color of the player. + The maximum number of players is 6, which occurs in Classic 3 with 3 + real players and 3 pseudo-players for the 4th color. + @note @ref libboardgame_avoid_stack_allocation */ +class Search final + : public libboardgame_mcts::SearchBase +{ +public: + Search(Variant initial_variant, unsigned nu_threads, size_t memory); + + ~Search(); + + unique_ptr create_state() override; + + PlayerInt get_nu_players() const override; + + PlayerInt get_player() const override; + + bool check_followup(ArrayList& sequence) override; + + string get_info() const override; + + + /** @name Parameters */ + /** @{ */ + + bool get_avoid_symmetric_draw() const; + + void set_avoid_symmetric_draw(bool enable); + + /** Automatically set some user-changeable parameters that have different + optimal values for different game variants whenever the game variant + changes. + Default is true. */ + bool get_auto_param() const; + + void set_auto_param(bool enable); + + /** @} */ // @name + + + bool search(Move& mv, const Board& bd, Color to_play, Float max_count, + size_t min_simulations, double max_time, + TimeSource& time_source); + + /** Get color to play at root node of the last search. */ + Color get_to_play() const; + + const History& get_last_history() const; + + /** Get board position of last search at root node as setup. + @param[out] variant + @param[out] setup */ + void get_root_position(Variant& variant, Setup& setup) const; + +protected: + void on_start_search(bool is_followup) override; + +private: + /** Automatically set default parameters for the game variant if + the game variant changes. */ + bool m_auto_param; + + /** Game variant of last search. */ + Variant m_variant; + + Color m_to_play; + + SharedConst m_shared_const; + + /** Local variable reused for efficiency. */ + History m_history; + + History m_last_history; + + const Board& get_board() const; + + void set_default_param(Variant variant); +}; + +inline bool Search::get_auto_param() const +{ + return m_auto_param; +} + +inline bool Search::get_avoid_symmetric_draw() const +{ + return m_shared_const.avoid_symmetric_draw; +} + +inline const Board& Search::get_board() const +{ + return *m_shared_const.board; +} + +inline const History& Search::get_last_history() const +{ + return m_last_history; +} + +inline PlayerInt Search::get_nu_players() const +{ + return m_variant != Variant::classic_3 ? get_board().get_nu_colors() : 6; +} + +inline PlayerInt Search::get_player() const +{ + auto to_play = m_to_play.to_int(); + if ( m_variant == Variant::classic_3 && to_play == 3) + return static_cast(to_play + get_board().get_alt_player()); + else + return to_play; +} + +inline Color Search::get_to_play() const +{ + return m_to_play; +} + +inline void Search::set_auto_param(bool enable) +{ + m_auto_param = enable; +} + +inline void Search::set_avoid_symmetric_draw(bool enable) +{ + m_shared_const.avoid_symmetric_draw = enable; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_SEARCH_H diff --git a/src/libpentobi_mcts/SearchParamConst.h b/src/libpentobi_mcts/SearchParamConst.h new file mode 100644 index 0000000..aaa8558 --- /dev/null +++ b/src/libpentobi_mcts/SearchParamConst.h @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/SearchParamConst.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_SEARCH_PARAM_CONST_H +#define LIBPENTOBI_MCTS_SEARCH_PARAM_CONST_H + +#include "Float.h" +#include "libpentobi_base/Board.h" +#include "libboardgame_mcts/PlayerMove.h" + +namespace libpentobi_mcts { + +using libboardgame_mcts::PlayerInt; +using libpentobi_base::Board; +using libpentobi_base::Color; + +//----------------------------------------------------------------------------- + +/** Optional compile-time parameters for libboardgame_mcts::Search. + See libboardgame_mcts::SearchParamConstDefault for the meaning of the + members. */ +struct SearchParamConst +{ + typedef libpentobi_mcts::Float Float; + + static const PlayerInt max_players = 6; + + /** The maximum number of moves in a simulation. + This needs to include pass moves because in the in-tree phase pass + moves (Move::null()) are used. The game ends after all colors have + passed in a row. Therefore, the maximum number of moves is reached in + case that a piece move is followed by (Color::range-1) pass moves and + an extra Color::range pass moves at the end. */ + static const unsigned max_moves = + Color::range * (Color::range * Board::max_pieces + 1); + +#ifdef LIBBOARDGAME_MCTS_SINGLE_THREAD + static const bool multithread = false; +#else + static const bool multithread = true; +#endif + + static const bool rave = true; + + static const bool rave_dist_weighting = true; + + static const bool use_lgr = true; + +#if PENTOBI_LOW_RESOURCES + static const size_t lgr_hash_table_size = (1 << 20); +#else + static const size_t lgr_hash_table_size = (1 << 21); +#endif + + static const bool virtual_loss = true; + + static const bool use_unlikely_change = true; + + static constexpr Float child_min_count = 3; + + static constexpr Float tie_value = 0.5f; + + static constexpr Float prune_count_start = 16; + + static constexpr double expected_sim_per_sec = 100; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_SEARCH_PARAM_CONST_H diff --git a/src/libpentobi_mcts/SharedConst.cpp b/src/libpentobi_mcts/SharedConst.cpp new file mode 100644 index 0000000..906d7da --- /dev/null +++ b/src/libpentobi_mcts/SharedConst.cpp @@ -0,0 +1,317 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/SharedConst.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SharedConst.h" + +namespace libpentobi_mcts { + +using libpentobi_base::BoardConst; +using libpentobi_base::BoardType; +using libpentobi_base::Piece; +using libpentobi_base::PieceSet; +using libpentobi_base::ScoreType; + +//----------------------------------------------------------------------------- + +namespace { + +void filter_min_size(const BoardConst& bc, ScoreType min_size, + PieceMap& is_piece_considered) +{ + for (Piece::IntType i = 0; i < bc.get_nu_pieces(); ++i) + { + Piece piece(i); + auto& piece_info = bc.get_piece_info(piece); + if (piece_info.get_score_points() < min_size) + is_piece_considered[piece] = false; + } +} + +/** Check if an adjacent status is a possible follow-up status for another + one. */ +inline bool is_followup_adj_status(unsigned status_new, unsigned status_old) +{ + return (status_new & status_old) == status_old; +} + +void set_piece_considered(const BoardConst& bc, const char* name, + PieceMap& is_piece_considered, + bool is_considered = true) +{ + Piece piece; + bool found = bc.get_piece_by_name(name, piece); + LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(found); + LIBBOARDGAME_ASSERT(found); + is_piece_considered[piece] = is_considered; +} + +void set_pieces_considered(const Board& bd, unsigned nu_moves, + PieceMap& is_piece_considered) +{ + auto& bc = bd.get_board_const(); + unsigned nu_colors = bd.get_nu_colors(); + is_piece_considered.fill(true); + switch (bc.get_board_type()) + { + case BoardType::duo: + if (nu_moves < 2 * nu_colors) + filter_min_size(bc, 5, is_piece_considered); + else if (nu_moves < 3 * nu_colors) + filter_min_size(bc, 4, is_piece_considered); + else if (nu_moves < 5 * nu_colors) + filter_min_size(bc, 3, is_piece_considered); + break; + case BoardType::classic: + if (nu_moves < nu_colors) + { + is_piece_considered.fill(false); + set_piece_considered(bc, "V5", is_piece_considered); + set_piece_considered(bc, "Z5", is_piece_considered); + } + else if (nu_moves < 2 * nu_colors) + { + filter_min_size(bc, 5, is_piece_considered); + set_piece_considered(bc, "F", is_piece_considered, false); + set_piece_considered(bc, "P", is_piece_considered, false); + set_piece_considered(bc, "T5", is_piece_considered, false); + set_piece_considered(bc, "U", is_piece_considered, false); + set_piece_considered(bc, "X", is_piece_considered, false); + } + else if (nu_moves < 3 * nu_colors) + { + filter_min_size(bc, 5, is_piece_considered); + set_piece_considered(bc, "P", is_piece_considered, false); + set_piece_considered(bc, "U", is_piece_considered, false); + } + else if (nu_moves < 5 * nu_colors) + filter_min_size(bc, 4, is_piece_considered); + else if (nu_moves < 7 * nu_colors) + filter_min_size(bc, 3, is_piece_considered); + break; + case BoardType::trigon: + case BoardType::trigon_3: + if (nu_moves < nu_colors) + { + is_piece_considered.fill(false); + set_piece_considered(bc, "V", is_piece_considered); + set_piece_considered(bc, "I6", is_piece_considered); + } + if (nu_moves < 4 * nu_colors) + { + filter_min_size(bc, 6, is_piece_considered); + // O is a bad early move, it neither extends, nor blocks well + set_piece_considered(bc, "O", is_piece_considered, false); + } + else if (nu_moves < 5 * nu_colors) + filter_min_size(bc, 5, is_piece_considered); + else if (nu_moves < 7 * nu_colors) + filter_min_size(bc, 4, is_piece_considered); + else if (nu_moves < 9 * nu_colors) + filter_min_size(bc, 3, is_piece_considered); + break; + case BoardType::nexos: + if (nu_moves < 3 * nu_colors) + filter_min_size(bc, 4, is_piece_considered); + else if (nu_moves < 5 * nu_colors) + filter_min_size(bc, 3, is_piece_considered); + break; + case BoardType::callisto: + case BoardType::callisto_2: + case BoardType::callisto_3: + is_piece_considered[bd.get_one_piece()] = false; + if (nu_moves < 3 * nu_colors) + filter_min_size(bc, 5, is_piece_considered); + else if (nu_moves < 8 * nu_colors) + filter_min_size(bc, 4, is_piece_considered); + else if (nu_moves < 12 * nu_colors) + filter_min_size(bc, 3, is_piece_considered); + break; + } +} + +} // namespace + +//----------------------------------------------------------------------------- + +SharedConst::SharedConst(const Color& to_play) + : board(nullptr), + to_play(to_play), + avoid_symmetric_draw(true) +{ } + +void SharedConst::init(bool is_followup) +{ + auto& bd = *board; + auto& bc = bd.get_board_const(); + + // Initialize precomp_moves + for (Color c : bd.get_colors()) + { + auto& precomp = precomp_moves[c]; + auto& old_precomp = (is_followup ? precomp : bc.get_precomp_moves()); + + m_is_forbidden.set(); + for (Point p : bd) + if (! bd.is_forbidden(p, c)) + { + auto adj_status = bd.get_adj_status(p, c); + for (Piece piece : bd.get_pieces_left(c)) + { + if (! old_precomp.has_moves(piece, p, adj_status)) + continue; + for (Move mv : old_precomp.get_moves(piece, p, adj_status)) + if (m_is_forbidden[mv] && ! bd.is_forbidden(c, mv)) + m_is_forbidden.clear(mv); + } + } + + // Don't use bd.get_pieces_left() because its ordering is not preserved + // during a game. The in-place construction requires that the loop + // iterates in the same order as during the last construction such that + // it doesn't overwrite elements it still needs to read. + Board::PiecesLeftList pieces; + for (Piece::IntType i = 0; i < bc.get_nu_pieces(); ++i) + if (bd.is_piece_left(c, Piece(i))) + pieces.push_back(Piece(i)); + if (! is_followup) + for (Point p : bd) + if (! bd.is_forbidden(p, c)) + { + auto adj_status = bd.get_adj_status(p, c); + for (unsigned i = 0; i < PrecompMoves::nu_adj_status; ++i) + if (is_followup_adj_status(i, adj_status)) + for (auto piece : pieces) + precomp.set_list_range(p, i, piece, 0, 0); + } + unsigned n = 0; + for (Point p : bd) + { + if (bd.is_forbidden(p, c)) + continue; + auto adj_status = bd.get_adj_status(p, c); + for (unsigned i = 0; i < PrecompMoves::nu_adj_status; ++i) + { + if (! is_followup_adj_status(i, adj_status)) + continue; + for (auto piece : pieces) + { + if (! old_precomp.has_moves(piece, p, i)) + continue; + auto begin = n; + for (auto& mv : old_precomp.get_moves(piece, p, i)) + if (! m_is_forbidden[mv]) + precomp.set_move(n++, mv); + precomp.set_list_range(p, i, piece, begin, n - begin); + } + } + } + } + + if (! is_followup) + init_pieces_considered(); + if (bd.get_piece_set() == PieceSet::callisto) + init_one_piece_callisto(is_followup); +} + +void SharedConst::init_one_piece_callisto(bool is_followup) +{ + auto& bd = *board; + auto& bc = bd.get_board_const(); + Piece one_piece = bd.get_one_piece(); + unsigned n = 0; + if (! is_followup) + { + for (Point p : bd) + if (! bd.is_center_section(p) && bd.get_point_state(p).is_empty()) + { + auto moves = bc.get_moves(one_piece, p, 0); + LIBBOARDGAME_ASSERT(moves.size() == 1); + Move mv = *moves.begin(); + if (! is_useless_one_piece_point(p)) + { + one_piece_points_callisto.get_unchecked(n) = p; + one_piece_moves_callisto.get_unchecked(n) = mv; + ++n; + } + } + } + else + for (unsigned i = 0; i < one_piece_points_callisto.size(); ++i) + { + Point p = one_piece_points_callisto[i]; + Move mv = one_piece_moves_callisto[i]; + if (bd.get_point_state(p).is_empty() + && ! is_useless_one_piece_point(p)) + { + one_piece_points_callisto.get_unchecked(n) = p; + one_piece_moves_callisto.get_unchecked(n) = mv; + ++n; + } + } + one_piece_points_callisto.resize(n); + one_piece_moves_callisto.resize(n); +} + +void SharedConst::init_pieces_considered() +{ + auto& bd = *board; + auto& bc = bd.get_board_const(); + is_piece_considered_list.clear(); + bool is_callisto = (bd.get_piece_set() == PieceSet::callisto); + for (auto i = bd.get_nu_onboard_pieces(); i < Board::max_game_moves; ++i) + { + PieceMap is_piece_considered; + set_pieces_considered(bd, i, is_piece_considered); + bool are_all_considered = true; + for (Piece::IntType j = 0; j < bc.get_nu_pieces(); ++j) + if (! is_piece_considered[Piece(j)] + && ! (is_callisto && Piece(j) == bd.get_one_piece())) + { + are_all_considered = false; + break; + } + if (are_all_considered) + { + min_move_all_considered = i; + break; + } + auto pos = find(is_piece_considered_list.begin(), + is_piece_considered_list.end(), + is_piece_considered); + if (pos != is_piece_considered_list.end()) + this->is_piece_considered[i] = &(*pos); + else + { + is_piece_considered_list.push_back(is_piece_considered); + this->is_piece_considered[i] = &is_piece_considered_list.back(); + } + } + is_piece_considered_all.fill(true); + if (is_callisto) + is_piece_considered_all[bd.get_one_piece()] = false; + is_piece_considered_none.fill(false); +} + +/** Check if a point is a useless move for the 1-piece. + @return true if all neighbors are occupied, because the 1-piece doesn't + contribute to the score and playing there neither enables own moves + nor prevents opponent moves with larger pieces. */ +bool SharedConst::is_useless_one_piece_point(Point p) const +{ + auto& bd = *board; + for (Point pp: bd.get_geometry().get_diag(p)) + if (bd.get_point_state(pp).is_empty()) + return false; + return true; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/SharedConst.h b/src/libpentobi_mcts/SharedConst.h new file mode 100644 index 0000000..a02d776 --- /dev/null +++ b/src/libpentobi_mcts/SharedConst.h @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/SharedConst.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_SHARED_CONST_H +#define LIBPENTOBI_MCTS_SHARED_CONST_H + +#include "libpentobi_base/Board.h" +#include "libpentobi_base/MoveMarker.h" + +namespace libpentobi_mcts { + +using namespace std; +using libboardgame_util::ArrayList; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::ColorMap; +using libpentobi_base::Move; +using libpentobi_base::MoveMarker; +using libpentobi_base::PieceMap; +using libpentobi_base::Point; +using libpentobi_base::PointList; +using libpentobi_base::PrecompMoves; + +//----------------------------------------------------------------------------- + +/** Constant data shared between the search states. */ +class SharedConst +{ +public: + /** Precomputed moves additionally constrained by moves that are + non-forbidden at root position. */ + ColorMap precomp_moves; + + /** The game board. + Contains the current position. */ + const Board* board; + + /** The color to play at the root of the search. */ + const Color& to_play; + + bool avoid_symmetric_draw; + + /** Minimum total number of pieces on the board where all pieces are + considered until the rest of the simulation. */ + unsigned min_move_all_considered; + + /** Precomputed lists of considered pieces depending on the total number + of pieces on the board. + Only initialized for numbers greater than or equal to the number in the + root position and less than min_move_all_considered. + Contains pointers to unique values such that the comparison of the + lists can be done by comparing the pointers to the lists. */ + array*, Board::max_game_moves> is_piece_considered; + + /** List of unique values for is_piece_considered. */ + ArrayList, Board::max_game_moves> is_piece_considered_list; + + /** Precomputed lists of considered pieces if all pieces are enforced to be + considered (because using the restricted set of pieces would generate + no moves). */ + PieceMap is_piece_considered_all; + + PieceMap is_piece_considered_none; + + /** List of legal points in the root position for the 1x1-piece in + Callisto. */ + PointList one_piece_points_callisto; + + /** Moves corresponding to one_piece_points_callisto. */ + ArrayList one_piece_moves_callisto; + + + explicit SharedConst(const Color& to_play); + + void init(bool is_followup); + +private: + /** Temporary variable used in init(). + Reused for efficiency. */ + MoveMarker m_is_forbidden; + + void init_one_piece_callisto(bool is_followup); + + void init_pieces_considered(); + + bool is_useless_one_piece_point(Point p) const; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_SHARED_CONST_H diff --git a/src/libpentobi_mcts/State.cpp b/src/libpentobi_mcts/State.cpp new file mode 100644 index 0000000..5b3da1c --- /dev/null +++ b/src/libpentobi_mcts/State.cpp @@ -0,0 +1,873 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/State.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "State.h" + +#include "libboardgame_util/MathUtil.h" +#include "libpentobi_base/ScoreUtil.h" +#if LIBBOARDGAME_DEBUG +#include "libpentobi_base/BoardUtil.h" +#endif + +namespace libpentobi_mcts { + +using libboardgame_util::fast_exp; +using libpentobi_base::get_multiplayer_result; +using libpentobi_base::BoardType; +using libpentobi_base::PointState; +using libpentobi_base::ScoreType; + +//----------------------------------------------------------------------------- + +namespace { + +/** Gamma value for PlayoutFeatures::get_nu_local(). + The value of nu_local dominates all other features, so we use a high + gamma. Above some limit, we don't care about the exact value. */ +float gamma_local[PlayoutFeatures::max_local + 1] = + { 1, 1e6f, 1e12f, 1e18f, 1e24f, 1e25f, 1e25f, 1e25f, 1e25f, 1e25f, 1e25f, + 1e25f, 1e25f, 1e25f, 1e25f }; + +inline Float sigmoid(Float steepness, Float x) +{ + return -1.f + 2.f / (1.f + fast_exp(-steepness * x)); +} + +} // namespace + +//----------------------------------------------------------------------------- + +State::State(Variant initial_variant, const SharedConst& shared_const) + : m_shared_const(shared_const), + m_bd(initial_variant) +{ +} + +template +inline void State::add_moves(Point p, Color c, + const Board::PiecesLeftList& pieces, + float& total_gamma, MoveList& moves, + unsigned& nu_moves) +{ + auto& marker = m_marker[c]; + auto& playout_features = m_playout_features[c]; + auto adj_status = m_bd.get_adj_status(p, c); + for (Piece piece : pieces) + { + if (! has_moves(c, piece, p, adj_status)) + continue; + auto gamma_piece = m_gamma_piece[piece]; + for (Move mv : get_moves(c, piece, p, adj_status)) + if (! marker[mv] + && check_move( + mv, get_move_info(mv), gamma_piece, moves, + nu_moves, playout_features, total_gamma)) + marker.set(mv); + } +} + +template +void State::add_one_piece_moves(Color c, bool with_gamma, float& total_gamma, + MoveList& moves, unsigned& nu_moves) +{ + Piece one_piece = m_bd.get_one_piece(); + auto nu_left = m_bd.get_nu_left_piece(c, one_piece); + if (nu_left == 0) + return; + for (unsigned i = 0; i < m_shared_const.one_piece_points_callisto.size(); + ++i) + { + Point p = m_shared_const.one_piece_points_callisto[i]; + if (m_bd.is_forbidden(p, c)) + continue; + Move mv = m_shared_const.one_piece_moves_callisto[i]; + LIBBOARDGAME_ASSERT(nu_moves < MoveList::max_size); + moves.get_unchecked(nu_moves) = mv; + ++nu_moves; + LIBBOARDGAME_ASSERT(! m_marker[c][mv]); + m_marker[c].set(mv); + if (with_gamma) + { + total_gamma += m_gamma_piece[one_piece]; + m_cumulative_gamma[nu_moves - 1] = total_gamma; + } + } +} + +template +void State::add_starting_moves(Color c, const Board::PiecesLeftList& pieces, + bool with_gamma, MoveList& moves) +{ + // Using only one starting point (if game variant has more than one) not + // only reduces the branching factor but is also necessary because + // update_moves() assumes that a move stays legal if the forbidden + // status for all of its points does not change. + Point p = find_best_starting_point(c); + if (p.is_null()) + return; + unsigned nu_moves = 0; + auto& marker = m_marker[c]; + auto& is_forbidden = m_bd.is_forbidden(c); + float total_gamma = 0; + for (Piece piece : pieces) + for (Move mv : get_moves(c, piece, p, 0)) + { + LIBBOARDGAME_ASSERT(! marker[mv]); + if (check_forbidden(is_forbidden, mv, moves, nu_moves)) + { + marker.set(mv); + if (with_gamma) + { + total_gamma += m_gamma_piece[piece]; + m_cumulative_gamma[nu_moves - 1] = total_gamma; + } + } + } + moves.resize(nu_moves); +} + +template +bool State::check_forbidden(const GridExt& is_forbidden, Move mv, + MoveList& moves, unsigned& nu_moves) +{ + auto p = get_move_info(mv).begin(); + unsigned forbidden = is_forbidden[*p]; + for (unsigned i = 1; i < MAX_SIZE; ++i) + // Logically, forbidden is a bool and the next line should be + // forbidden = forbidden || is_forbidden[*(++p)] + // But this generates branches, which are bad for performance in this + // tight loop (unrolled by the compiler). So we use a bitwise OR, which + // works because C++ guarantees that true/false converts to 1/0. + forbidden |= static_cast(is_forbidden[*(++p)]); + if (forbidden != 0) + return false; + LIBBOARDGAME_ASSERT(nu_moves < MoveList::max_size); + moves.get_unchecked(nu_moves) = mv; + ++nu_moves; + return true; +} + +template +bool State::check_move(Move mv, const MoveInfo& info, + float gamma_piece, MoveList& moves, unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma) +{ + LIBBOARDGAME_ASSERT(IS_CALLISTO == m_is_callisto); + auto p = info.begin(); + PlayoutFeatures::Compute features(*p, playout_features); + for (unsigned i = 1; i < MAX_SIZE; ++i) + features.add(*(++p), playout_features); + if (features.is_forbidden()) + return false; + auto gamma = gamma_piece; + if (! (IS_CALLISTO && info.get_size() == 1)) + gamma *= gamma_local[features.get_nu_local()]; + total_gamma += gamma; + m_cumulative_gamma[nu_moves] = total_gamma; + LIBBOARDGAME_ASSERT(nu_moves < MoveList::max_size); + moves.get_unchecked(nu_moves) = mv; + ++nu_moves; + return true; +} + +template +inline bool State::check_move(Move mv, const MoveInfo& info, + MoveList& moves, unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma) +{ + return check_move( + mv, info, m_gamma_piece[info.get_piece()], moves, nu_moves, + playout_features, total_gamma); +} + +#if LIBBOARDGAME_DEBUG +string State::dump() const +{ + ostringstream s; + s << "pentobi_mcts::State:\n" << libpentobi_base::boardutil::dump(m_bd); + return s.str(); +} +#endif + +/** Evaluation function for game variants with 2 players and 2 colors per + player. */ +void State::evaluate_multicolor(array& result) +{ + LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2); + LIBBOARDGAME_ASSERT(m_bd.get_nu_colors() == 4); + // Always evaluate symmetric positions in trigon_2 as a draw in the + // playouts. See comment in evaluate_playout_duo. + // m_is_symmetry_broken is always true in classic_2, no need to check for + // game variant. + if (! m_is_symmetry_broken + && m_bd.get_nu_onboard_pieces() >= m_symmetry_min_nu_pieces) + { + if (log_simulations) + LIBBOARDGAME_LOG("Result: 0.5 (symmetry)"); + result[0] = result[1] = result[2] = result[3] = 0.5; + return; + } + + auto s = m_bd.get_score_multicolor(Color(0)); + Float res; + if (s > 0) + res = 1; + else if (s < 0) + res = 0; + else + res = 0.5; + if (log_simulations) + LIBBOARDGAME_LOG("Result color 0: sco=", s, " game_res=", res); + res += get_quality_bonus(Color(0), res, s) + + get_quality_bonus_attach_multicolor(); + if (log_simulations) + LIBBOARDGAME_LOG("res=", res); + result[0] = result[2] = res; + result[1] = result[3] = 1.f - res; +} + +/** Evaluation function for game variants with more than 2 players. + The result is 0,0.5,1 for loss/tie/win in 2-player variants. For n \> 2 + players, this is generalized in the following way: The scores are sorted in + ascending order. Each rank r_i (i in 0..n-1) is assigned a result value of + r_i/(n-1). If multiple players have the same score, the result value is the + average of all ranks with this score. So being the single winner still + gives the result 1 and having the lowest score gives the result 0. Being + the single winner is better than sharing the best place, which is better + than getting the second place, etc. */ +void State::evaluate_multiplayer(array& result) +{ + auto nu_players = m_bd.get_nu_players(); + LIBBOARDGAME_ASSERT(nu_players > 2); + array points; + for (Color::IntType i = 0; i < nu_players; ++i) + points[i] = m_bd.get_points(Color(i)); + array game_result; + get_multiplayer_result(nu_players, points, game_result, m_is_callisto); + for (Color::IntType i = 0; i < nu_players; ++i) + { + Color c(i); + auto s = m_bd.get_score_multiplayer(c); + result[i] = game_result[i] + get_quality_bonus(c, game_result[i], s); + if (log_simulations) + LIBBOARDGAME_LOG("Result sco=", s, " game_res=", game_result[i], + " res=", result[i]); + } + if (m_bd.get_variant() == Variant::classic_3) + { + result[3] = result[0]; + result[4] = result[1]; + result[5] = result[2]; + } +} + +/** Evaluation function for Duo, Junior and Callisto Two-Player. */ +void State::evaluate_twocolor(array& result) +{ + LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2); + LIBBOARDGAME_ASSERT(m_bd.get_nu_colors() == 2); + ScoreType s; + if (! m_is_symmetry_broken + && m_bd.get_nu_onboard_pieces() >= m_symmetry_min_nu_pieces) + { + if (log_simulations) + LIBBOARDGAME_LOG("Symmetry not broken"); + s = 0; + } + else + s = m_bd.get_score_twocolor(Color(0)); + Float res; + if (s > 0) + res = 1; + else if (s < 0 || (m_is_callisto && s == 0)) + res = 0; + else + res = 0.5; + if (log_simulations) + LIBBOARDGAME_LOG("Result sco=", s, " game_res=", res); + res += get_quality_bonus(Color(0), res, s); + if (m_is_callisto) + res += get_quality_bonus_attach_twocolor(); + if (log_simulations) + LIBBOARDGAME_LOG("res=", res); + result[0] = res; + result[1] = 1.f - res; +} + +Point State::find_best_starting_point(Color c) const +{ + // We use the starting point that maximizes the distance to occupied + // starting points, especially to the ones occupied by the player (their + // distance is weighted with a factor of 2). + Point best = Point::null(); + float max_distance = -1; + auto board_type = m_bd.get_board_type(); + bool is_trigon = (board_type == BoardType::trigon + || board_type == BoardType::trigon_3); + bool is_nexos = board_type == BoardType::nexos; + float ratio = (is_trigon ? 1.732f : 1); + auto& geo = m_bd.get_geometry(); + for (Point p : m_bd.get_starting_points(c)) + { + if (m_bd.is_forbidden(p, c)) + continue; + if (is_nexos) + { + // Don't use the starting segments towards the edge of the board + auto x = geo.get_x(p); + if (x <= 3 || x >= geo.get_width() - 3 - 1) + continue; + auto y = geo.get_y(p); + if (y <= 3 || y >= geo.get_height() - 3 - 1) + continue; + } + float px = static_cast(geo.get_x(p)); + float py = static_cast(geo.get_y(p)); + float d = 0; + for (Color i : Color::Range(m_nu_colors)) + for (Point pp : m_bd.get_starting_points(i)) + { + PointState s = m_bd.get_point_state(pp); + if (! s.is_empty()) + { + float ppx = static_cast(geo.get_x(pp)); + float ppy = static_cast(geo.get_y(pp)); + float dx = ppx - px; + float dy = ratio * (ppy - py); + float weight = 1; + if (s == c || s == m_bd.get_second_color(c)) + weight = 2; + d += weight * sqrt(dx * dx + dy * dy); + } + } + if (d > max_distance) + { + best = p; + max_distance = d; + } + } + return best; +} + +bool State::gen_children(Tree::NodeExpander& expander, Float root_val) +{ + if (m_nu_passes == m_nu_colors) + return true; + Color to_play = m_bd.get_to_play(); + if (m_max_piece_size == 5) + { + init_moves_without_gamma<5>(to_play); + return m_prior_knowledge.gen_children<5, 16>(m_bd, m_moves[to_play], + m_is_symmetry_broken, + expander, root_val); + } + else if (m_max_piece_size == 6) + { + init_moves_without_gamma<6>(to_play); + return m_prior_knowledge.gen_children<6, 22>(m_bd, m_moves[to_play], + m_is_symmetry_broken, + expander, root_val); + } + else + { + LIBBOARDGAME_ASSERT(m_max_piece_size == 7); + init_moves_without_gamma<7>(to_play); + return m_prior_knowledge.gen_children<7, 12>(m_bd, m_moves[to_play], + m_is_symmetry_broken, + expander, root_val); + } +} + +bool State::gen_playout_move_full(PlayerMove& mv) +{ + Color to_play = m_bd.get_to_play(); + while (true) + { + if (! m_is_move_list_initialized[to_play]) + { + if (m_max_piece_size == 5) + { + if (m_is_callisto) + init_moves_with_gamma<5, 16, true>(to_play); + else + init_moves_with_gamma<5, 16, false>(to_play); + } + else if (m_max_piece_size == 6) + init_moves_with_gamma<6, 22, false>(to_play); + else + init_moves_with_gamma<7, 12, false>(to_play); + } + else if (m_has_moves[to_play]) + { + if (m_max_piece_size == 5) + { + if (m_is_callisto) + update_moves<5, 16, true>(to_play); + else + update_moves<5, 16, false>(to_play); + } + else if (m_max_piece_size == 6) + update_moves<6, 22, false>(to_play); + else + update_moves<7, 12, false>(to_play); + } + if ((m_has_moves[to_play] = ! m_moves[to_play].empty())) + break; + if (++m_nu_passes == m_nu_colors) + return false; + if (m_check_terminate_early && m_bd.get_score_twoplayer(to_play) < 0 + && ! m_has_moves[m_bd.get_second_color(to_play)]) + { + if (log_simulations) + LIBBOARDGAME_LOG("Terminate early (no moves and neg. score)"); + return false; + } + to_play = to_play.get_next(m_nu_colors); + m_bd.set_to_play(to_play); + // Don't try to handle symmetry after pass moves + m_is_symmetry_broken = true; + } + + auto& moves = m_moves[to_play]; + LIBBOARDGAME_ASSERT(! moves.empty()); + auto total_gamma = m_cumulative_gamma[moves.size() - 1]; + if (log_simulations) + LIBBOARDGAME_LOG("Moves: ", moves.size(), ", total_gamma: ", + total_gamma); + auto begin = m_cumulative_gamma.begin(); + auto end = begin + moves.size(); + auto random = m_random.generate_float(0, total_gamma); + auto pos = lower_bound(begin, end, random); + LIBBOARDGAME_ASSERT(pos != end); + mv = PlayerMove(get_player(), + moves[static_cast(pos - begin)]); + return true; +} + +string State::get_info() const +{ + ostringstream s; + if (m_bd.get_nu_players() == 2) + { + s << "Sco: "; + m_stat_score[Color(0)].write(s, true, 1); + } + s << '\n'; + return s.str(); +} + +inline const PieceMap& State::get_is_piece_considered(Color c) const +{ + if (m_is_callisto + && m_bd.get_nu_left_piece(c, m_bd.get_one_piece()) > 1) + return m_shared_const.is_piece_considered_none; + // Use number of on-board pieces for move number to handle the case where + // there are more pieces on the board than moves (setup positions) + unsigned nu_moves = m_bd.get_nu_onboard_pieces(); + if (nu_moves >= m_shared_const.min_move_all_considered + || m_force_consider_all_pieces) + return m_shared_const.is_piece_considered_all; + return *m_shared_const.is_piece_considered[nu_moves]; +} + +/** Initializes and returns m_pieces_considered if not all pieces are + considered, otherwise m_bd.get_pieces_left(c) is returned. */ +inline const Board::PiecesLeftList& State::get_pieces_considered(Color c) +{ + auto is_piece_considered = m_is_piece_considered[c]; + auto& pieces_left = m_bd.get_pieces_left(c); + if (is_piece_considered == &m_shared_const.is_piece_considered_all + && ! m_is_callisto) + return pieces_left; + unsigned n = 0; + for (Piece piece : pieces_left) + if ((*is_piece_considered)[piece]) + m_pieces_considered.get_unchecked(n++) = piece; + m_pieces_considered.resize(n); + return m_pieces_considered; +} + +/** Basic bonus added to the result for quality-based rewards. + See also: Pepels et al.: Quality-based Rewards for Monte-Carlo Tree Search + Simulations. ECAI 2014. */ +inline Float State::get_quality_bonus(Color c, Float result, Float score) +{ + Float bonus = 0; + + // Game length + Float l = static_cast(m_bd.get_nu_moves()); + m_stat_len.add(l); + Float var = m_stat_len.get_variance(); + if (var > 0) + bonus += -0.12f * (result - 0.5f) + * sigmoid(2.f, (l - m_stat_len.get_mean()) / sqrt(var)); + + // Game score + auto& stat = m_stat_score[c]; + stat.add(score); + var = stat.get_variance(); + if (var > 0) + bonus += 0.3f * sigmoid(2.f, (score - stat.get_mean()) / sqrt(var)); + return bonus; +} + +/** Additional quality-based rewards based on number of attach points. + The number of non-forbidden attach points is another feature of a superior + final position. Only used in some two-player variants, mainly helps in + Trigon. */ +inline Float State::get_quality_bonus_attach_twocolor() +{ + LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2); + int n = m_bd.get_attach_points(Color(0)).size() + - m_bd.get_attach_points(Color(1)).size(); + for (Point p : m_bd.get_attach_points(Color(0))) + n -= m_bd.is_forbidden(p, Color(0)); + for (Point p : m_bd.get_attach_points(Color(1))) + n += m_bd.is_forbidden(p, Color(1)); + Float attach = static_cast(n); + m_stat_attach.add(attach); + auto var = m_stat_attach.get_variance(); + if (var > 0) + return 0.1f * sigmoid(2.f, + (attach - m_stat_attach.get_mean()) / sqrt(var)); + return 0; +} + +/** Like get_quality_bonus_attach_twocolor() but for 2 colors per player. */ +inline Float State::get_quality_bonus_attach_multicolor() +{ + LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2); + LIBBOARDGAME_ASSERT(m_bd.get_nu_colors() == 4); + int n = m_bd.get_attach_points(Color(0)).size() + + m_bd.get_attach_points(Color(2)).size() + - m_bd.get_attach_points(Color(1)).size() + - m_bd.get_attach_points(Color(3)).size(); + for (Point p : m_bd.get_attach_points(Color(0))) + n -= m_bd.is_forbidden(p, Color(0)); + for (Point p : m_bd.get_attach_points(Color(2))) + n -= m_bd.is_forbidden(p, Color(2)); + for (Point p : m_bd.get_attach_points(Color(1))) + n += m_bd.is_forbidden(p, Color(1)); + for (Point p : m_bd.get_attach_points(Color(3))) + n += m_bd.is_forbidden(p, Color(3)); + Float attach = static_cast(n); + m_stat_attach.add(attach); + auto var = m_stat_attach.get_variance(); + if (var > 0) + return 0.1f * sigmoid(2.f, + (attach - m_stat_attach.get_mean()) / sqrt(var)); + return 0; +} + +template +void State::init_moves_with_gamma(Color c) +{ + m_is_piece_considered[c] = &get_is_piece_considered(c); + m_playout_features[c].set_local(m_bd); + auto& marker = m_marker[c]; + auto& moves = m_moves[c]; + marker.clear(moves); + auto& pieces = get_pieces_considered(c); + if (m_bd.is_first_piece(c) && ! (MAX_SIZE == 5 && m_is_callisto)) + add_starting_moves(c, pieces, true, moves); + else + { + unsigned nu_moves = 0; + float total_gamma = 0; + if (MAX_SIZE == 5 && m_is_callisto) + add_one_piece_moves(c, true, total_gamma, moves, + nu_moves); + if (m_is_piece_considered[c] + != &m_shared_const.is_piece_considered_none) + for (Point p : m_bd.get_attach_points(c)) + { + if (m_bd.is_forbidden(p, c)) + continue; + add_moves(p, c, pieces, total_gamma, + moves, nu_moves); + m_moves_added_at[c][p] = true; + } + moves.resize(nu_moves); + } + m_is_move_list_initialized[c] = true; + m_nu_new_moves[c] = 0; + m_last_attach_points_end[c] = m_bd.get_attach_points(c).end(); + if (moves.empty() && + m_is_piece_considered[c] + != &m_shared_const.is_piece_considered_all) + { + m_force_consider_all_pieces = true; + init_moves_with_gamma(c); + } +} + +template +void State::init_moves_without_gamma(Color c) +{ + m_is_piece_considered[c] = &get_is_piece_considered(c); + auto& marker = m_marker[c]; + auto& moves = m_moves[c]; + marker.clear(moves); + auto& pieces = get_pieces_considered(c); + auto& is_forbidden = m_bd.is_forbidden(c); + if (m_bd.is_first_piece(c) && ! (MAX_SIZE == 5 && m_is_callisto)) + add_starting_moves(c, pieces, false, moves); + else + { + unsigned nu_moves = 0; + if (MAX_SIZE == 5 && m_is_callisto) + { + float total_gamma_dummy; + add_one_piece_moves(c, false, total_gamma_dummy, moves, + nu_moves); + } + if (m_is_piece_considered[c] + != &m_shared_const.is_piece_considered_none) + for (Point p : m_bd.get_attach_points(c)) + { + if (is_forbidden[p]) + continue; + auto adj_status = m_bd.get_adj_status(p, c); + for (Piece piece : pieces) + { + if (! has_moves(c, piece, p, adj_status)) + continue; + for (Move mv : get_moves(c, piece, p, adj_status)) + if (! marker[mv] + && check_forbidden( + is_forbidden, mv, moves, nu_moves)) + marker.set(mv); + } + m_moves_added_at[c][p] = true; + } + moves.resize(nu_moves); + } + m_is_move_list_initialized[c] = true; + m_nu_new_moves[c] = 0; + m_last_attach_points_end[c] = m_bd.get_attach_points(c).end(); + if (moves.empty() && + m_is_piece_considered[c] + != &m_shared_const.is_piece_considered_all) + { + m_force_consider_all_pieces = true; + init_moves_without_gamma(c); + } +} + +void State::play_expanded_child(Move mv) +{ + if (log_simulations) + LIBBOARDGAME_LOG("Playing expanded child"); + if (! mv.is_null()) + play_playout(mv); + else + { + ++m_nu_passes; + m_bd.set_to_play(m_bd.get_to_play().get_next(m_nu_colors)); + // Don't try to handle pass moves: a pass move either breaks symmetry + // or both players have passed and it's the end of the game and we need + // symmetry detection only as a heuristic (playouts and move value + // initialization) + m_is_symmetry_broken = true; + if (log_simulations) + LIBBOARDGAME_LOG(m_bd); + } +} + +void State::start_search() +{ + auto& bd = *m_shared_const.board; + m_bd.copy_from(bd); + m_bd.set_to_play(m_shared_const.to_play); + m_bd.take_snapshot(); + m_nu_colors = bd.get_nu_colors(); + m_is_callisto = (bd.get_piece_set() == PieceSet::callisto); + for (Color c : Color::Range(m_nu_colors)) + m_playout_features[c].init_snapshot(m_bd, c); + m_bc = &m_bd.get_board_const(); + m_max_piece_size = m_bc->get_max_piece_size(); + m_move_info_array = m_bc->get_move_info_array(); + m_move_info_ext_array = m_bc->get_move_info_ext_array(); + m_check_terminate_early = + (bd.get_nu_moves() < 10u * m_nu_colors + && m_bd.get_nu_players() == 2); + auto variant = bd.get_variant(); + m_check_symmetric_draw = + (has_central_symmetry(variant) + && ! ((m_shared_const.to_play == Color(1) + || m_shared_const.to_play == Color(3)) + && m_shared_const.avoid_symmetric_draw) + && ! check_symmetry_broken(bd)); + if (! m_check_symmetric_draw) + // Pretending that the symmetry is always broken is equivalent to + // ignoring symmetric draws + m_is_symmetry_broken = true; + if (variant == Variant::trigon_2 || variant == Variant::callisto_2) + m_symmetry_min_nu_pieces = 5; + else + { + LIBBOARDGAME_ASSERT(! m_check_symmetric_draw || variant == Variant::duo + || variant == Variant::junior); + m_symmetry_min_nu_pieces = 3; + } + + m_prior_knowledge.start_search(bd); + m_stat_len.clear(); + m_stat_attach.clear(); + for (Color c : Color::Range(m_nu_colors)) + m_stat_score[c].clear(); + + // Init gamma values + float gamma_size_factor = 1; + float gamma_nu_attach_factor = 1; + switch (bd.get_board_type()) + { + case BoardType::classic: + gamma_size_factor = 5; + break; + case BoardType::duo: + gamma_size_factor = 3; + gamma_nu_attach_factor = 1.8f; + break; + case BoardType::trigon: + case BoardType::trigon_3: // Not tuned + gamma_size_factor = 5; + break; + case BoardType::nexos: // Not tuned + gamma_size_factor = 5; + gamma_nu_attach_factor = 1.8f; + break; + case BoardType::callisto_2: + case BoardType::callisto: // Not tuned + case BoardType::callisto_3: // Not tuned + gamma_size_factor = 12; + gamma_nu_attach_factor = 1.8f; + break; + } + for (Piece::IntType i = 0; i < m_bc->get_nu_pieces(); ++i) + { + Piece piece(i); + auto score_points = m_bc->get_piece_info(piece).get_score_points(); + auto piece_nu_attach = + static_cast(m_bc->get_nu_attach_points(piece)); + LIBBOARDGAME_ASSERT(score_points >= 0); + LIBBOARDGAME_ASSERT(piece_nu_attach > 0); + m_gamma_piece[piece] = + pow(gamma_size_factor, score_points) + * pow(gamma_nu_attach_factor, piece_nu_attach - 1); + } +} + +void State::start_simulation(size_t n) +{ +#if LIBBOARDGAME_DISABLE_LOG + LIBBOARDGAME_UNUSED(n); +#endif + if (log_simulations) + LIBBOARDGAME_LOG("=================================================\n", + "Simulation ", n, "\n", + "================================================="); + m_bd.restore_snapshot(); + m_force_consider_all_pieces = false; + auto& geo = m_bd.get_geometry(); + for (Color c : Color::Range(m_nu_colors)) + { + m_has_moves[c] = true; + m_is_move_list_initialized[c] = false; + m_playout_features[c].restore_snapshot(m_bd); + m_moves_added_at[c].fill(false, geo); + } + m_nu_passes = 0; +} + +template +void State::update_moves(Color c) +{ + auto& playout_features = m_playout_features[c]; + playout_features.set_local(m_bd); + + auto& marker = m_marker[c]; + + // Find old moves that are still legal + auto& is_forbidden = m_bd.is_forbidden(c); + auto& moves = m_moves[c]; + unsigned nu_moves = 0; + float total_gamma = 0; + Piece piece; + if (m_nu_new_moves[c] == 1 && + ! m_bd.is_piece_left( + c, (piece = + get_move_info(m_last_move[c]).get_piece()))) + for (Move mv : moves) + { + auto& info = get_move_info(mv); + if (info.get_piece() == piece + || ! check_move( + mv, info, moves, nu_moves, playout_features, + total_gamma)) + marker.clear(mv); + } + else + for (Move mv : moves) + { + auto& info = get_move_info(mv); + if (! m_bd.is_piece_left(c, info.get_piece()) + || ! check_move( + mv, info, moves, nu_moves, playout_features, + total_gamma)) + marker.clear(mv); + } + + // Find new legal moves because of new pieces played by this color + auto& pieces = get_pieces_considered(c); + auto& attach_points = m_bd.get_attach_points(c); + auto begin = m_last_attach_points_end[c]; + auto end = attach_points.end(); + for (auto i = begin; i != end; ++i) + if (! is_forbidden[*i] && ! m_moves_added_at[c][*i]) + { + m_moves_added_at[c][*i] = true; + add_moves(*i, c, pieces, total_gamma, moves, + nu_moves); + } + m_nu_new_moves[c] = 0; + m_last_attach_points_end[c] = end; + + // Generate moves for pieces not considered in the last position + if (m_is_piece_considered[c] != &m_shared_const.is_piece_considered_all) + { + auto& is_piece_considered = *m_is_piece_considered[c]; + if (nu_moves == 0) + m_force_consider_all_pieces = true; + auto& is_piece_considered_new = get_is_piece_considered(c); + if (&is_piece_considered != &is_piece_considered_new) + { + Board::PiecesLeftList new_pieces; + unsigned n = 0; + for (Piece piece : m_bd.get_pieces_left(c)) + if (! is_piece_considered[piece] + && is_piece_considered_new[piece]) + new_pieces.get_unchecked(n++) = piece; + new_pieces.resize(n); + for (Point p : attach_points) + if (! is_forbidden[p]) + add_moves( + p, c, new_pieces, total_gamma, moves, nu_moves); + m_is_piece_considered[c] = &is_piece_considered_new; + } + } + moves.resize(nu_moves); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/State.h b/src/libpentobi_mcts/State.h new file mode 100644 index 0000000..84fd37a --- /dev/null +++ b/src/libpentobi_mcts/State.h @@ -0,0 +1,545 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/State.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_STATE_H +#define LIBPENTOBI_MCTS_STATE_H + +#include "PlayoutFeatures.h" +#include "PriorKnowledge.h" +#include "SharedConst.h" +#include "StateUtil.h" +#include "libboardgame_mcts/LastGoodReply.h" +#include "libboardgame_mcts/PlayerMove.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/RandomGenerator.h" +#include "libboardgame_util/Statistics.h" + +namespace libpentobi_mcts { + +using libboardgame_mcts::LastGoodReply; +using libboardgame_mcts::PlayerInt; +using libboardgame_mcts::PlayerMove; +using libboardgame_util::RandomGenerator; +using libboardgame_util::Statistics; +using libpentobi_base::BoardConst; +using libpentobi_base::MoveInfo; +using libpentobi_base::MoveInfoExt; +using libpentobi_base::Piece; +using libpentobi_base::PieceInfo; +using libpentobi_base::PieceSet; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** A state of a simulation. + This class contains modifiable data used in a simulation. In multi-threaded + search (not yet implemented), each thread uses its own instance of this + class. + This class incrementally keeps track of the legal moves. + The randomization in the playouts is done by assigning a heuristically + tuned gamma value to each move. The gamma value determines the probabilty + that a move is played in the playout phase. */ +class State +{ +public: + typedef libboardgame_mcts::Node + Node; + + typedef libboardgame_mcts::Tree Tree; + + typedef libboardgame_mcts::LastGoodReply + LastGoodReply; + + /** Constructor. + @param initial_variant Game variant to initialize the internal + board with (may avoid unnecessary BoardConst creation for game variant + that is never used) + @param shared_const (@ref libboardgame_doc_storesref) */ + State(Variant initial_variant, const SharedConst& shared_const); + + State& operator=(const State&) = delete; + + /** Play a move in the in-tree phase of the search. */ + void play_in_tree(Move mv); + + /** Handle end of in-tree phase. */ + void finish_in_tree(); + + /** Play a move right after expanding a node. */ + void play_expanded_child(Move mv); + + /** Finish in-tree phase without expanding a node. */ + void finish_in_tree_no_expansion(); + + /** Get current player to play. */ + PlayerInt get_player() const; + + void start_search(); + + void start_simulation(size_t n); + + bool gen_children(Tree::NodeExpander& expander, Float root_val); + + void start_playout() { } + + /** Generate a playout move. + @return @c false if end of game was reached, and no move was + generated. */ + bool gen_playout_move(const LastGoodReply& lgr, Move last, + Move second_last, PlayerMove& move); + + void evaluate_playout(array& result); + + void play_playout(Move mv); + + /** Do not update RAVE values for n'th move of the current simulation. */ + bool skip_rave(Move mv) const; + +#if LIBBOARDGAME_DEBUG + string dump() const; +#endif + + string get_info() const; + +private: + static const bool log_simulations = false; + + /** The cumulative gamma value of the moves in m_moves. */ + array m_cumulative_gamma; + + Color::IntType m_nu_passes; + + const SharedConst& m_shared_const; + + Board m_bd; + + const BoardConst* m_bc; + + Color::IntType m_nu_colors; + + BoardConst::MoveInfoArray m_move_info_array; + + BoardConst::MoveInfoExtArray m_move_info_ext_array; + + /** Incrementally updated lists of legal moves for both colors. + Only the move list for the color to play van be used in any given + position, the other color is not updated immediately after a move. */ + ColorMap m_moves; + + ColorMap*> m_is_piece_considered; + + /** The list of pieces considered in the current move if not all pieces + are considered. */ + Board::PiecesLeftList m_pieces_considered; + + PriorKnowledge m_prior_knowledge; + + /** Gamma value for a piece. */ + PieceMap m_gamma_piece; + + /** Number of moves played by a color since the last update of its move + list. */ + ColorMap m_nu_new_moves; + + /** Board::get_attach_points().end() for a color at the last update of + its move list. */ + ColorMap m_last_attach_points_end; + + /** Last move played by a color since the last update of its move list. */ + ColorMap m_last_move; + + ColorMap m_is_move_list_initialized; + + ColorMap m_has_moves; + + /** Marks moves contained in m_moves. */ + ColorMap m_marker; + + ColorMap m_playout_features; + + RandomGenerator m_random; + + /** Used in get_quality_bonus(). */ + ColorMap> m_stat_score; + + /** Used in get_quality_bonus(). */ + Statistics m_stat_len; + + /** Used in get_quality_bonus(). */ + Statistics m_stat_attach; + + bool m_check_symmetric_draw; + + bool m_check_terminate_early; + + bool m_is_symmetry_broken; + + /** Enforce all pieces to be considered for the rest of the simulation. + This applies to all colors, because it is only used if no moves were + generated because not all pieces were considered and this case is so + rare that it is not worth the cost of setting such a flag for each + color individually. */ + bool m_force_consider_all_pieces; + + bool m_is_callisto; + + /** Minimum number of pieces on board to perform a symmetry check. + 3 in Duo/Junior or 5 in Trigon because this is the earliest move number + to break the symmetry. The early playout termination that evaluates all + symmetric positions as a draw should not be used earlier because it can + cause bad move selection in very short searches if all moves are + evaluated as draw and the search is not deep enough to find that the + symmetry can be broken a few moves later. */ + unsigned m_symmetry_min_nu_pieces; + + /** Cache of m_bc->get_max_piece_size() */ + unsigned m_max_piece_size; + + /** Remember attach points that were already used for move generation. + Allows the incremental update of the move lists to skip attach points + of newly played pieces that were already attach points of previously + played pieces. */ + ColorMap> m_moves_added_at; + + + template + void add_moves(Point p, Color c, const Board::PiecesLeftList& pieces, + float& total_gamma, MoveList& moves, unsigned& nu_moves); + + template + LIBBOARDGAME_NOINLINE + void add_starting_moves(Color c, const Board::PiecesLeftList& pieces, + bool with_gamma, MoveList& moves); + + template + LIBBOARDGAME_NOINLINE + void add_one_piece_moves(Color c, bool with_gamma, float& total_gamma, + MoveList& moves, unsigned& nu_moves); + + void evaluate_multicolor(array& result); + + void evaluate_multiplayer(array& result); + + void evaluate_twocolor(array& result); + + Point find_best_starting_point(Color c) const; + + Float get_quality_bonus(Color c, Float result, Float score); + + Float get_quality_bonus_attach_twocolor(); + + Float get_quality_bonus_attach_multicolor(); + + template + const MoveInfo& get_move_info(Move mv) const; + + template + const MoveInfoExt& get_move_info_ext(Move mv) const; + + PrecompMoves::Range get_moves(Color c, Piece piece, Point p, + unsigned adj_status) const; + + bool has_moves(Color c, Piece piece, Point p, unsigned adj_status) const; + + const PieceMap& get_is_piece_considered(Color c) const; + + const Board::PiecesLeftList& get_pieces_considered(Color c); + + template + void init_moves_with_gamma(Color c); + + template + void init_moves_without_gamma(Color c); + + template + bool check_forbidden(const GridExt& is_forbidden, Move mv, + MoveList& moves, unsigned& nu_moves); + + bool check_lgr(Move mv) const; + + template + bool check_move(Move mv, const MoveInfo& info, float gamma_piece, + MoveList& moves, unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma); + + template + bool check_move(Move mv, const MoveInfo& info, MoveList& moves, + unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma); + + bool gen_playout_move_full(PlayerMove& mv); + + template + void update_moves(Color c); + + template + void update_playout_features(Color c, Move mv); + + template + LIBBOARDGAME_NOINLINE void update_symmetry_broken(Move mv); +}; + +/** Check if last-good-reply move is applicable. + To be faster, it doesn't check for starting moves because such moves rarely + occur in the playout phase and doesn't check if a 1-piece move is in the + center in Callisto because such moves are not generated in the search. */ +inline bool State::check_lgr(Move mv) const +{ + if (mv.is_null()) + return false; + Color c = m_bd.get_to_play(); + auto piece = m_bd.get_move_piece(mv); + if (! m_bd.is_piece_left(c, piece)) + return false; + auto points = m_bd.get_move_points(mv); + auto i = points.begin(); + auto end = points.end(); + bool has_attach_point = false; + do + { + if (m_bd.is_forbidden(*i, c)) + return false; + // Logically, we mean: + // has_attach_point = has_attach_point || is_attach_point(*i, c) + // But this generates branches, which are bad for performance in this + // tight loop (unrolled by the compiler). So we use a bitwise OR, which + // works because C++ guarantees that true/false converts to 1/0. + has_attach_point |= m_bd.is_attach_point(*i, c); + } + while (++i != end); + if (m_is_callisto) + { + Piece one_piece = m_bd.get_one_piece(); + if (piece == one_piece) + return true; + if (m_bd.get_nu_left_piece(c, one_piece) > 1 && piece != one_piece) + return false; + } + return has_attach_point; +} + +inline void State::evaluate_playout(array& result) +{ + auto nu_players = m_bd.get_nu_players(); + if (nu_players == 2) + { + if (m_nu_colors == 2) + evaluate_twocolor(result); + else + evaluate_multicolor(result); + } + else + evaluate_multiplayer(result); +} + +inline void State::finish_in_tree() +{ + if (log_simulations) + LIBBOARDGAME_LOG("Finish in-tree"); + if (m_check_symmetric_draw) + m_is_symmetry_broken = check_symmetry_broken(m_bd); +} + +inline bool State::gen_playout_move(const LastGoodReply& lgr, Move last, + Move second_last, PlayerMove& mv) +{ + if (m_nu_passes == m_nu_colors) + return false; + if (! m_is_symmetry_broken + && m_bd.get_nu_onboard_pieces() >= m_symmetry_min_nu_pieces) + { + // See also the comment in evaluate_playout() + if (log_simulations) + LIBBOARDGAME_LOG("Terminate playout. Symmetry not broken."); + return false; + } + PlayerInt player = get_player(); + Move lgr2 = lgr.get_lgr2(player, last, second_last); + if (check_lgr(lgr2)) + { + if (log_simulations) + LIBBOARDGAME_LOG("Playing last good reply 2"); + mv = PlayerMove(player, lgr2); + return true; + } + Move lgr1 = lgr.get_lgr1(player, last); + if (check_lgr(lgr1)) + { + if (log_simulations) + LIBBOARDGAME_LOG("Playing last good reply 1"); + mv = PlayerMove(player, lgr1); + return true; + } + return gen_playout_move_full(mv); +} + +template +inline const MoveInfo& State::get_move_info(Move mv) const +{ + LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves()); + return BoardConst::get_move_info(mv, m_move_info_array); +} + +template +inline const MoveInfoExt& State::get_move_info_ext( + Move mv) const +{ + LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves()); + return BoardConst::get_move_info_ext( + mv, m_move_info_ext_array); +} + +inline PrecompMoves::Range State::get_moves(Color c, Piece piece, Point p, + unsigned adj_status) const +{ + return m_shared_const.precomp_moves[c].get_moves(piece, p, adj_status); +} + +inline PlayerInt State::get_player() const +{ + unsigned player = m_bd.get_to_play().to_int(); + if ( m_bd.get_variant() == Variant::classic_3 && player == 3) + player += m_bd.get_alt_player(); + return static_cast(player); +} + +inline bool State::has_moves(Color c, Piece piece, Point p, + unsigned adj_status) const +{ + return m_shared_const.precomp_moves[c].has_moves(piece, p, adj_status); +} + +inline void State::play_in_tree(Move mv) +{ + Color to_play = m_bd.get_to_play(); + if (! mv.is_null()) + { + LIBBOARDGAME_ASSERT(m_bd.is_legal(to_play, mv)); + m_nu_passes = 0; + if (m_max_piece_size == 5) + { + m_bd.play<5, 16>(to_play, mv); + update_playout_features<5, 16>(to_play, mv); + } + else if (m_max_piece_size == 6) + { + m_bd.play<6, 22>(to_play, mv); + update_playout_features<6, 22>(to_play, mv); + } + else + { + m_bd.play<7, 12>(to_play, mv); + update_playout_features<7, 12>(to_play, mv); + } + } + else + { + ++m_nu_passes; + m_bd.set_to_play(to_play.get_next(m_nu_colors)); + } + if (log_simulations) + LIBBOARDGAME_LOG(m_bd); +} + +inline void State::play_playout(Move mv) +{ + auto to_play = m_bd.get_to_play(); + LIBBOARDGAME_ASSERT(m_bd.is_legal(to_play, mv)); + if (m_max_piece_size == 5) + { + m_bd.play<5, 16>(to_play, mv); + update_playout_features<5, 16>(to_play, mv); + if (! m_is_symmetry_broken) + update_symmetry_broken<5>(mv); + } + else if (m_max_piece_size == 6) + { + m_bd.play<6, 22>(to_play, mv); + update_playout_features<6, 22>(to_play, mv); + if (! m_is_symmetry_broken) + update_symmetry_broken<6>(mv); + } + else + { + m_bd.play<7, 12>(to_play, mv); + update_playout_features<7, 12>(to_play, mv); + // No game variant with piece size 7 uses m_is_symmetry_broken + } + ++m_nu_new_moves[to_play]; + m_last_move[to_play] = mv; + m_nu_passes = 0; + if (log_simulations) + LIBBOARDGAME_LOG(m_bd); +} + +inline bool State::skip_rave(Move mv) const +{ + LIBBOARDGAME_UNUSED(mv); + return false; +} + +template +inline void State::update_playout_features(Color c, Move mv) +{ + auto& info = get_move_info(mv); + for (Color i : Color::Range(m_nu_colors)) + m_playout_features[i].set_forbidden(info); + m_playout_features[c].set_forbidden( + get_move_info_ext(mv)); +} + +template +void State::update_symmetry_broken(Move mv) +{ + Color to_play = m_bd.get_to_play(); + Color second_color = m_bd.get_second_color(to_play); + auto& symmetric_points = m_bc->get_symmetrc_points(); + auto& info = get_move_info(mv); + auto i = info.begin(); + auto end = info.end(); + if (to_play == Color(0) || to_play == Color(2)) + { + // First player to play: Check that all symmetric points of the last + // move of the second player are occupied by the first player + do + { + Point symm_p = symmetric_points[*i]; + if (m_bd.get_point_state(symm_p) != second_color) + { + m_is_symmetry_broken = true; + return; + } + } + while (++i != end); + } + else + { + // Second player to play: Check that all symmetric points of the last + // move of the first player are empty (i.e. the second player can play + // there to preserve the symmetry) + do + { + Point symm_p = symmetric_points[*i]; + if (! m_bd.get_point_state(symm_p).is_empty()) + { + m_is_symmetry_broken = true; + return; + } + } + while (++i != end); + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_STATE_H diff --git a/src/libpentobi_mcts/StateUtil.cpp b/src/libpentobi_mcts/StateUtil.cpp new file mode 100644 index 0000000..c85c2f5 --- /dev/null +++ b/src/libpentobi_mcts/StateUtil.cpp @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/StateUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "StateUtil.h" + +namespace libpentobi_mcts { + +using libpentobi_base::Color; +using libpentobi_base::ColorMove; +using libpentobi_base::Geometry; +using libpentobi_base::Point; +using libpentobi_base::PointState; + +//----------------------------------------------------------------------------- + +namespace { + +array symmetric_state = + { Color(1), Color(0), Color(3), Color(2) }; + +} // namespace + +//----------------------------------------------------------------------------- + +bool check_symmetry_broken(const Board& bd) +{ + LIBBOARDGAME_ASSERT(has_central_symmetry(bd.get_variant())); + auto& symmetric_points = bd.get_board_const().get_symmetrc_points(); + Color to_play = bd.get_to_play(); + auto& geo = bd.get_geometry(); + // No need to iterator over the whole board when checking symmetry (this + // makes the assumption that the symmetric points of the points in the + // first half of the integer range are in the second half). + Geometry::Iterator begin = geo.begin(); + LIBBOARDGAME_ASSERT(geo.get_range() % 2 == 0); + Geometry::Iterator end(static_cast(geo.get_range() / 2)); +#if LIBBOARDGAME_DEBUG + for (auto p = begin; p != end; ++p) + LIBBOARDGAME_ASSERT(symmetric_points[*p].to_int() >= (*end).to_int()); +#endif + if (to_play == Color(0) || to_play == Color(2)) + { + // First player to play: the symmetry is broken if the position is + // not symmetric. + for (auto p = begin; p != end; ++p) + { + PointState s1 = bd.get_point_state(*p); + if (! s1.is_empty()) + { + Point symm_p = symmetric_points[*p]; + PointState s2 = bd.get_point_state(symm_p); + if (s2 != symmetric_state[s1.to_int()]) + return true; + } + } + } + else + { + // Second player to play: the symmetry is broken if the second player + // cannot copy the first player's last move to make the position + // symmetric again. + unsigned nu_moves = bd.get_nu_moves(); + if (nu_moves == 0) + // Don't try to handle the case if the second player has to play as + // first move (e.g. in setup positions) + return true; + Color previous_color = bd.get_previous(to_play); + ColorMove last_mv = bd.get_move(nu_moves - 1); + if (last_mv.color != previous_color) + // Don't try to handle non-alternating moves in board history + return true; + auto points = bd.get_move_points(last_mv.move); + for (Point p : points) + if (! bd.get_point_state(symmetric_points[p]).is_empty()) + return true; + for (auto p = begin; p != end; ++p) + { + PointState s1 = bd.get_point_state(*p); + if (! s1.is_empty()) + { + PointState s2 = bd.get_point_state(symmetric_points[*p]); + if (s2 != symmetric_state[s1.to_int()] + && ! points.contains(*p)) + return true; + } + } + } + return false; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/StateUtil.h b/src/libpentobi_mcts/StateUtil.h new file mode 100644 index 0000000..9431948 --- /dev/null +++ b/src/libpentobi_mcts/StateUtil.h @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/StateUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_STATE_UTIL_H +#define LIBPENTOBI_MCTS_STATE_UTIL_H + +#include "libpentobi_base/Board.h" + +namespace libpentobi_mcts { + +using namespace std; +using libpentobi_base::Board; + +//----------------------------------------------------------------------------- + +bool check_symmetry_broken(const Board& bd); + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_STATE_UTIL_H diff --git a/src/libpentobi_mcts/Util.cpp b/src/libpentobi_mcts/Util.cpp new file mode 100644 index 0000000..0430c75 --- /dev/null +++ b/src/libpentobi_mcts/Util.cpp @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Util.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Util.h" + +#include +#include "libboardgame_sgf/Writer.h" +#include "libboardgame_util/Log.h" +#include "libpentobi_base/BoardUtil.h" +#include "libpentobi_base/PentobiSgfUtil.h" + +namespace libpentobi_mcts { +namespace util { + +using libboardgame_mcts::Node; +using libboardgame_mcts::Tree; +using libboardgame_sgf::Writer; +using libpentobi_base::boardutil::write_setup; +using libpentobi_base::sgf_util::get_color_id; + +//----------------------------------------------------------------------------- + +namespace { + +void dump_tree_recurse(Writer& writer, Variant variant, + const Search::Tree& tree, const Search::Node& node, + Color to_play) +{ + ostringstream comment; + comment << "Visits: " << node.get_visit_count() + << "\nVal: " << node.get_value() + << "\nCnt: " << node.get_value_count(); + writer.write_property("C", comment.str()); + writer.end_node(); + Color next_to_play = to_play.get_next(get_nu_colors(variant)); + vector children; + for (auto& i : tree.get_children(node)) + children.push_back(&i); + sort(children.begin(), children.end(), compare_node); + for (const auto i : children) + { + writer.begin_tree(); + writer.begin_node(); + auto mv = i->get_move(); + if (! mv.is_null()) + { + auto& board_const = BoardConst::get(variant); + auto id = get_color_id(variant, to_play); + if (! mv.is_null()) + writer.write_property(id, board_const.to_string(mv, false)); + } + dump_tree_recurse(writer, variant, tree, *i, next_to_play); + writer.end_tree(); + } +} + +} // namespace + +//----------------------------------------------------------------------------- + +bool compare_node(const Search::Node* n1, const Search::Node* n2) +{ + Float count1 = n1->get_visit_count(); + Float count2 = n2->get_visit_count(); + if (count1 != count2) + return count1 > count2; + return n1->get_value() > n2->get_value(); +} + +void dump_tree(ostream& out, const Search& search) +{ + Variant variant; + Setup setup; + search.get_root_position(variant, setup); + Writer writer(out); + writer.begin_tree(); + writer.begin_node(); + writer.write_property("GM", to_string(variant)); + write_setup(writer, variant, setup); + writer.write_property("PL", get_color_id(variant, setup.to_play)); + auto& tree = search.get_tree(); + dump_tree_recurse(writer, variant, tree, tree.get_root(), setup.to_play); + writer.end_tree(); +} + +unsigned get_nu_threads() +{ + unsigned nu_threads = thread::hardware_concurrency(); + if (nu_threads == 0) + { + LIBBOARDGAME_LOG("Could not determine the number of hardware threads"); + nu_threads = 1; + } + // The lock-free search probably scales up to 16-32 threads, but we + // haven't tested more than 4 threads, we still use single precision + // float for LIBBOARDGAME_MCTS_FLOAT_TYPE (which limits the maximum number + // of simulations per search) and CPUs with more than 4 cores are + // currently not very common anyway. Also, the loss of playing strength + // of a multi-threaded search with the same count as a single-threaded + // search will become larger with many threads, so there would need to be + // a correction factor in the number of simulations per level to take this + // into account. + if (nu_threads > 4) + nu_threads = 4; + return nu_threads; +} + +//----------------------------------------------------------------------------- + +} // namespace util +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/Util.h b/src/libpentobi_mcts/Util.h new file mode 100644 index 0000000..eb96978 --- /dev/null +++ b/src/libpentobi_mcts/Util.h @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Util.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_UTIL_H +#define LIBPENTOBI_MCTS_UTIL_H + +#include "Search.h" + +namespace libpentobi_mcts { +namespace util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Comparison function for sorting children of a node by count. + Prefers nodes with higher counts. Uses the node value as a tie breaker. */ +bool compare_node(const Search::Node* n1, const Search::Node* n2); + +/** Dump the search tree in SGF format. */ +void dump_tree(ostream& out, const Search& search); + +/** Suggest how many threads to use in the search depending on the current + system. */ +unsigned get_nu_threads(); + +//----------------------------------------------------------------------------- + +} // namespace util +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_UTIL_H diff --git a/src/libpentobi_thumbnail/CMakeLists.txt b/src/libpentobi_thumbnail/CMakeLists.txt new file mode 100644 index 0000000..77acc34 --- /dev/null +++ b/src/libpentobi_thumbnail/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(pentobi_thumbnail STATIC + CreateThumbnail.h + CreateThumbnail.cpp +) + +target_link_libraries(pentobi_thumbnail Qt5::Widgets) diff --git a/src/libpentobi_thumbnail/CreateThumbnail.cpp b/src/libpentobi_thumbnail/CreateThumbnail.cpp new file mode 100644 index 0000000..3aef69b --- /dev/null +++ b/src/libpentobi_thumbnail/CreateThumbnail.cpp @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_thumbnail/CreateThumbnail.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "CreateThumbnail.h" + +#include +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_util/StringUtil.h" +#include "libpentobi_base/NodeUtil.h" +#include "libpentobi_gui/BoardPainter.h" + +using namespace std; +using libboardgame_sgf::SgfNode; +using libboardgame_sgf::TreeReader; +using libboardgame_util::split; +using libboardgame_util::trim; +using libpentobi_base::get_board_type; +using libpentobi_base::Geometry; +using libpentobi_base::Grid; +using libpentobi_base::PieceSet; +using libpentobi_base::PointState; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +namespace { + +/** Helper function for getFinalPosition() */ +void handleSetup(const char* id, Color c, const SgfNode& node, + const Geometry& geo, Grid& pointState, + Grid& pieceId, unsigned& currentPieceId) +{ + vector values = node.get_multi_property(id); + for (const string& s : values) + { + if (trim(s).empty()) + continue; + vector v = split(s, ','); + ++currentPieceId; + for (const string& p_str : v) + { + Point p; + if (geo.from_string(p_str, p)) + { + pointState[p] = PointState(c); + pieceId[p] = currentPieceId; + } + } + } +} + +/** Helper function for getFinalPosition() */ +void handleSetupEmpty(const SgfNode& node, const Geometry& geo, + Grid& pointState, Grid& pieceId) +{ + vector values = node.get_multi_property("AE"); + for (const auto& s : values) + { + if (trim(s).empty()) + continue; + vector v = split(s, ','); + for (const auto& p_str : v) + { + Point p; + if (geo.from_string(p_str, p)) + { + pointState[p] = PointState::empty(); + pieceId[p] = 0; + } + } + } +} + +/** Get the board state of the final position of the main variation. + Avoids constructing an instance of a Tree or Game, which would do a costly + initialization of BoardConst and slow down the thumbnailer + unnecessarily. */ +bool getFinalPosition(const SgfNode& root, Variant& variant, + const Geometry*& geo, Grid& pointState, + Grid& pieceId) +{ + if (! parse_variant(root.get_property("GM", ""), variant)) + return false; + geo = &get_geometry(variant); + pointState.fill(PointState::empty(), *geo); + auto pieceSet = get_piece_set(variant); + if (pieceSet == PieceSet::nexos || pieceSet == PieceSet::callisto) + pieceId.fill(0, *geo); + auto node = &root; + unsigned id = 0; + while (node) + { + if (libpentobi_base::node_util::has_setup(*node)) + { + handleSetup("AB", Color(0), *node, *geo, pointState, pieceId, id); + handleSetup("AW", Color(1), *node, *geo, pointState, pieceId, id); + handleSetup("A1", Color(0), *node, *geo, pointState, pieceId, id); + handleSetup("A2", Color(1), *node, *geo, pointState, pieceId, id); + handleSetup("A3", Color(2), *node, *geo, pointState, pieceId, id); + handleSetup("A4", Color(3), *node, *geo, pointState, pieceId, id); + handleSetupEmpty(*node, *geo, pointState, pieceId); + if (node == &root) + // If the file starts with a setup (e.g. a puzzle), we use this + // position for the thumbnail. + break; + } + Color c; + MovePoints points; + if (libpentobi_base::node_util::get_move(*node, variant, c, points)) + { + ++id; + for (Point p : points) + { + pointState[p] = PointState(c); + pieceId[p] = id; + } + } + node = node->get_first_child_or_null(); + } + return true; +} + +} // namespace + +//----------------------------------------------------------------------------- + +bool createThumbnail(const QString& path, int width, int height, + QImage& image) +{ + TreeReader reader; + reader.set_read_only_main_variation(true); + reader.read(path.toLocal8Bit().constData()); + auto variant = + Variant::classic; // Initialize to avoid compiler warning + const Geometry* geo; + Grid pointState; + Grid pieceId; + if (! getFinalPosition(reader.get_tree(), variant, geo, pointState, + pieceId)) + { + cerr << "Not a valid Blokus SGF file\n"; + return false; + } + QPainter painter; + if (! painter.begin(&image)) + return false; + BoardPainter boardPainter; + boardPainter.paintEmptyBoard(painter, width, height, variant, *geo); + boardPainter.paintPieces(painter, pointState, pieceId); + painter.end(); + return true; +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_thumbnail/CreateThumbnail.h b/src/libpentobi_thumbnail/CreateThumbnail.h new file mode 100644 index 0000000..f2b1e50 --- /dev/null +++ b/src/libpentobi_thumbnail/CreateThumbnail.h @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_thumbnail/CreateThumbnail.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_THUMBNAIL_CREATE_THUMBNAIL_H +#define LIBPENTOBI_THUMBNAIL_CREATE_THUMBNAIL_H + +class QImage; +class QString; + +//----------------------------------------------------------------------------- + +bool createThumbnail(const QString& path, int width, int height, + QImage& image); + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_THUMBNAIL_CREATE_THUMBNAIL_H diff --git a/src/pentobi/AnalyzeGameWidget.cpp b/src/pentobi/AnalyzeGameWidget.cpp new file mode 100644 index 0000000..0e762fb --- /dev/null +++ b/src/pentobi/AnalyzeGameWidget.cpp @@ -0,0 +1,231 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeGameWidget.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "AnalyzeGameWidget.h" + +#include +#include +#include +#include +#include +#include +#include "Util.h" +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_util/Abort.h" +#include "libpentobi_gui/Util.h" + +using libboardgame_sgf::util::find_root; +using libboardgame_sgf::util::is_main_variation; +using libboardgame_util::set_abort; +using libboardgame_util::ArrayList; +using libpentobi_base::Board; +using libpentobi_base::PentobiTree; + +//----------------------------------------------------------------------------- + +AnalyzeGameWidget::AnalyzeGameWidget(QWidget* parent) + : QWidget(parent) +{ + setMinimumSize(240, 120); + m_isInitialized = false; + m_currentPosition = -1; +} + +void AnalyzeGameWidget::cancel() +{ + if (! m_isRunning) + return; + set_abort(); + m_future.waitForFinished(); +} + +void AnalyzeGameWidget::initSize() +{ + m_borderX = width() / 50; + m_borderY = height() / 20; + m_maxX = width() - 2 * m_borderX; + m_dX = qreal(m_maxX) / Board::max_game_moves; + m_maxY = height() - 2 * m_borderY; +} + +void AnalyzeGameWidget::mousePressEvent(QMouseEvent* event) +{ + if (! m_isInitialized && m_isRunning) + return; + unsigned moveNumber = + static_cast((event->x() - m_borderX) / m_dX); + if (moveNumber >= m_analyzeGame.get_nu_moves()) + return; + vector moves; + for (unsigned i = 0; i < moveNumber; ++i) + moves.push_back(m_analyzeGame.get_move(i)); + emit gotoPosition(m_analyzeGame.get_variant(), moves); +} + +void AnalyzeGameWidget::paintEvent(QPaintEvent*) +{ + if (! m_isInitialized) + return; + QPainter painter(this); + QFont font; + font.setStyleStrategy(QFont::PreferOutline); + // QFont::setPixelSize(0) prints a warning even if it works and the docs + // of Qt 5.3 don't forbid it (unlike QFont::setPointSize(0)). + font.setPixelSize(max(1, static_cast(0.06 * height()))); + QFontMetrics metrics(font); + painter.translate(m_borderX, m_borderY); + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(240, 240, 240)); + painter.drawRect(0, 0, m_maxX, m_maxY); + unsigned nu_moves = m_analyzeGame.get_nu_moves(); + if (m_currentPosition >= 0 + && static_cast(m_currentPosition) < nu_moves) + { + QPen pen(QColor(96, 96, 96)); + pen.setStyle(Qt::DotLine); + painter.setPen(pen); + int x = static_cast(m_currentPosition * m_dX + 0.5 * m_dX); + painter.drawLine(x, 0, x, m_maxY); + } + painter.setPen(QColor(32, 32, 32)); + painter.drawLine(0, 0, m_maxX, 0); + painter.drawLine(0, m_maxY, m_maxX, m_maxY); + painter.setRenderHint(QPainter::Antialiasing, true); + QString labelWin = tr("Win"); + QRect boundingRectWin = metrics.boundingRect(labelWin); + painter.drawText(QRect(0, 0, boundingRectWin.width(), + boundingRectWin.height()), + Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, + labelWin); + QString labelLoss = tr("Loss"); + QRect boundingRectLoss = metrics.boundingRect(labelLoss); + painter.drawText(QRect(0, m_maxY - boundingRectLoss.height(), + boundingRectLoss.width(), boundingRectLoss.height()), + Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip, + labelLoss); + painter.setRenderHint(QPainter::Antialiasing, false); + painter.setPen(QColor(128, 128, 128)); + painter.drawLine(0, m_maxY / 2, m_maxX, m_maxY / 2); + painter.setRenderHint(QPainter::Antialiasing, true); + for (unsigned i = 0; i < nu_moves; ++i) + { + double value = m_analyzeGame.get_value(i); + // Values can be outside [0..1] due to score/length bonuses + if (value < 0) + value = 0; + else if (value > 1) + value = 1; + auto color = Util::getPaintColor(m_analyzeGame.get_variant(), + m_analyzeGame.get_move(i).color); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawEllipse(QPointF((i + 0.5) * m_dX, (1 - value) * m_maxY), + 0.5 * m_dX, 0.5 * m_dX); + } +} + +void AnalyzeGameWidget::resizeEvent(QResizeEvent*) +{ + if (! m_isInitialized) + return; + initSize(); +} + +void AnalyzeGameWidget::setCurrentPosition(const Game& game, + const SgfNode& node) +{ + update(); + m_currentPosition = -1; + if (is_main_variation(node)) + { + ArrayList moves; + auto& tree = game.get_tree(); + auto current = &find_root(node); + while (current) + { + auto mv = tree.get_move(*current); + if (! mv.is_null() && moves.size() < Board::max_game_moves) + moves.push_back(mv); + if (current == &node) + break; + current = current->get_first_child_or_null(); + } + if (moves.size() <= m_analyzeGame.get_nu_moves()) + { + for (unsigned i = 0; i < moves.size(); ++i) + if (moves[i] != m_analyzeGame.get_move(i)) + return; + m_currentPosition = moves.size(); + } + } +} + +void AnalyzeGameWidget::showProgress(int progress) +{ + // m_progressDialog might already be closed if cancel was pressed and + // setValue makes it visible again (only with some Qt versions/platforms?) + if (m_progressDialog->isVisible()) + m_progressDialog->setValue(progress); + // Repaint the window with the current status of the analysis + update(); +} + +QSize AnalyzeGameWidget::sizeHint() const +{ + auto geo = QApplication::desktop()->screenGeometry(); + return QSize(geo.width() / 2, geo.height() / 3); +} + +void AnalyzeGameWidget::start(const Game& game, Search& search, + size_t nuSimulations) +{ + m_isInitialized = true; + m_game = &game; + m_search = &search; + m_nuSimulations = nuSimulations; + initSize(); + if (! m_progressDialog) + { + m_progressDialog = new QProgressDialog(this); + m_progressDialog->setWindowModality(Qt::WindowModal); + m_progressDialog->setWindowFlags(m_progressDialog->windowFlags() + & ~Qt::WindowContextHelpButtonHint); + m_progressDialog->setLabel(new QLabel(tr("Running game analysis..."), + this)); + Util::setNoTitle(*m_progressDialog); + m_progressDialog->setMinimumDuration(0); + connect(m_progressDialog, SIGNAL(canceled()), SLOT(cancel())); + } + m_progressDialog->show(); + m_isRunning = true; + m_future = QtConcurrent::run(this, &AnalyzeGameWidget::threadFunction); +} + +void AnalyzeGameWidget::threadFunction() +{ + // This function and the progress callback are not called from the GUI + // thread. So we need to invoke showProgress() with invokeMethod(). + auto progressCallback = + [&](unsigned movesAnalyzed, unsigned totalMoves) + { + if (totalMoves == 0) + return; + int progress = 100 * movesAnalyzed / totalMoves; + QMetaObject::invokeMethod(this, "showProgress", + Qt::BlockingQueuedConnection, + Q_ARG(int, progress)); + }; + m_analyzeGame.run(*m_game, *m_search, m_nuSimulations, progressCallback); + QMetaObject::invokeMethod(m_progressDialog, "hide", Qt::QueuedConnection); + m_isRunning = false; + emit finished(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/AnalyzeGameWidget.h b/src/pentobi/AnalyzeGameWidget.h new file mode 100644 index 0000000..a549a26 --- /dev/null +++ b/src/pentobi/AnalyzeGameWidget.h @@ -0,0 +1,117 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeGameWidget.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_ANALYZE_GAME_WIDGET_H +#define PENTOBI_ANALYZE_GAME_WIDGET_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include "libpentobi_mcts/AnalyzeGame.h" + +class QProgressDialog; + +using namespace std; +using libboardgame_sgf::SgfNode; +using libpentobi_base::ColorMove; +using libpentobi_base::Game; +using libpentobi_base::Variant; +using libpentobi_mcts::AnalyzeGame; +using libpentobi_mcts::Search; + +//----------------------------------------------------------------------------- + +class AnalyzeGameWidget + : public QWidget +{ + Q_OBJECT + +public slots: + /** Cancel a running analysis. + The function waits for the analysis to finish. The finished() signal + will still be invoked. */ + void cancel(); + +public: + explicit AnalyzeGameWidget(QWidget* parent); + + /** Start an analysis. + This function will return after the analysis has started but the + window will be protected by a modal cancelable progress dialog. + Don't modify the game or use the search from a different thread until + the signal finished() was emitted. This will walk through every game + position in the main variation and use the search to evaluate + positions. During the analysis, the parent window is protected with a + modal progress dialog. */ + void start(const Game& game, Search& search, size_t nuSimulations); + + /** Mark the current position. + Will clear the current position if the target node is not in the + main variation or does not correspond to a move in the move + sequence when the analysis was done. */ + void setCurrentPosition(const Game& game, const SgfNode& node); + + QSize sizeHint() const override; + +signals: + /** Tells that the analysis has finished. */ + void finished(); + + void gotoPosition(Variant variant, const vector& moves); + +protected: + void mousePressEvent(QMouseEvent* event) override; + + void paintEvent(QPaintEvent* event) override; + + void resizeEvent(QResizeEvent* event) override; + +private slots: + void showProgress(int progress); + +private: + bool m_isInitialized; + + bool m_isRunning; + + const Game* m_game; + + Search* m_search; + + size_t m_nuSimulations; + + AnalyzeGame m_analyzeGame; + + QProgressDialog* m_progressDialog = nullptr; + + QFuture m_future; + + int m_borderX; + + int m_borderY; + + qreal m_dX; + + int m_maxX; + + int m_maxY; + + /** Current position that will be marked or -1 if no position is marked. */ + int m_currentPosition; + + void initSize(); + + void threadFunction(); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_ANALYZE_GAME_WIDGET_H diff --git a/src/pentobi/AnalyzeGameWindow.cpp b/src/pentobi/AnalyzeGameWindow.cpp new file mode 100644 index 0000000..5f4a238 --- /dev/null +++ b/src/pentobi/AnalyzeGameWindow.cpp @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeGameWindow.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "AnalyzeGameWindow.h" + +#include +#include + +//----------------------------------------------------------------------------- + +AnalyzeGameWindow::AnalyzeGameWindow(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(tr("Game Analysis")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto layout = new QVBoxLayout; + setLayout(layout); + analyzeGameWidget = new AnalyzeGameWidget(this); + layout->addWidget(analyzeGameWidget); + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); + layout->addWidget(buttonBox); + connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); + buttonBox->setFocus(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/AnalyzeGameWindow.h b/src/pentobi/AnalyzeGameWindow.h new file mode 100644 index 0000000..d546fca --- /dev/null +++ b/src/pentobi/AnalyzeGameWindow.h @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeGameWindow.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_ANALYZE_GAME_WINDOW_H +#define PENTOBI_ANALYZE_GAME_WINDOW_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "AnalyzeGameWidget.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +class AnalyzeGameWindow final + : public QDialog +{ + Q_OBJECT + +public: + AnalyzeGameWidget* analyzeGameWidget; + + + explicit AnalyzeGameWindow(QWidget* parent); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_ANALYZE_GAME_WINDOW_H diff --git a/src/pentobi/AnalyzeSpeedDialog.cpp b/src/pentobi/AnalyzeSpeedDialog.cpp new file mode 100644 index 0000000..3251b68 --- /dev/null +++ b/src/pentobi/AnalyzeSpeedDialog.cpp @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeSpeedDialog.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "AnalyzeSpeedDialog.h" + +//----------------------------------------------------------------------------- + +AnalyzeSpeedDialog::AnalyzeSpeedDialog(QWidget* parent, const QString& title) + : QInputDialog(parent) +{ + m_items << tr("Fast") << tr("Normal") << tr("Slow"); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setWindowTitle(title); + setLabelText(tr("Analysis speed:")); + setInputMode(QInputDialog::TextInput); + setComboBoxItems(m_items); + setComboBoxEditable(false); +} + +AnalyzeSpeedDialog::~AnalyzeSpeedDialog() +{ +} + +void AnalyzeSpeedDialog::accept() +{ + m_speedValue = m_items.indexOf(textValue()); + QDialog::accept(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/AnalyzeSpeedDialog.h b/src/pentobi/AnalyzeSpeedDialog.h new file mode 100644 index 0000000..5bd73b2 --- /dev/null +++ b/src/pentobi/AnalyzeSpeedDialog.h @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeSpeedDialog.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_ANALYZE_SPEED_DIALOG_H +#define PENTOBI_ANALYZE_SPEED_DIALOG_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +//----------------------------------------------------------------------------- + +class AnalyzeSpeedDialog final + : public QInputDialog +{ + Q_OBJECT + +public: + AnalyzeSpeedDialog(QWidget* parent, const QString& title); + + ~AnalyzeSpeedDialog(); + + /** Get return value if dialog was accepted. + 0 = fast, 1 = normal, 2 = slow */ + int getSpeedValue() { return m_speedValue; } + +public slots: + void accept() override; + +private: + int m_speedValue = 0; + + QStringList m_items; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_ANALYZE_SPEED_DIALOG_H diff --git a/src/pentobi/Application.cpp b/src/pentobi/Application.cpp new file mode 100644 index 0000000..176adf4 --- /dev/null +++ b/src/pentobi/Application.cpp @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Application.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Application.h" + +#include "ShowMessage.h" +#include "libboardgame_sys/Compiler.h" + +using namespace std; +using libboardgame_sys::get_type_name; + +//----------------------------------------------------------------------------- + +bool Application::notify(QObject* receiver, QEvent* event) +{ + try + { + return QApplication::notify(receiver, event); + } + catch (const exception& e) + { + string detailedText = get_type_name(e) + ": " + e.what(); + showFatal(QString::fromLocal8Bit(detailedText.c_str())); + } + return false; +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/Application.h b/src/pentobi/Application.h new file mode 100644 index 0000000..8e497fe --- /dev/null +++ b/src/pentobi/Application.h @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Application.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_APPLICATION_H +#define PENTOBI_APPLICATION_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +//----------------------------------------------------------------------------- + +class Application + : public QApplication +{ + Q_OBJECT + +public: + using QApplication::QApplication; + + /** Reimplemented from QApplication::notify(). + Catches exceptions and shows an error message. */ + bool notify(QObject* receiver, QEvent* event) override; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_APPLICATION_H diff --git a/src/pentobi/CMakeLists.txt b/src/pentobi/CMakeLists.txt new file mode 100644 index 0000000..ecae6de --- /dev/null +++ b/src/pentobi/CMakeLists.txt @@ -0,0 +1,142 @@ +set(CMAKE_AUTOMOC TRUE) + +set(pentobi_SRCS + AnalyzeGameWidget.h + AnalyzeGameWidget.cpp + AnalyzeGameWindow.h + AnalyzeGameWindow.cpp + AnalyzeSpeedDialog.h + AnalyzeSpeedDialog.cpp + Application.h + Application.cpp + Main.cpp + MainWindow.h + MainWindow.cpp + RatedGamesList.h + RatedGamesList.cpp + RatingDialog.h + RatingDialog.cpp + RatingGraph.h + RatingGraph.cpp + RatingHistory.h + RatingHistory.cpp + ShowMessage.h + ShowMessage.cpp + Util.h + Util.cpp + pentobi.rc +) + +set(pentobi_ICNS + pentobi.png + pentobi-16.png + pentobi-32.png + pentobi-backward.png + pentobi-backward-16.png + pentobi-beginning.png + pentobi-beginning-16.png + pentobi-computer-colors.png + pentobi-computer-colors-16.png + pentobi-end.png + pentobi-end-16.png + pentobi-flip-horizontal.png + pentobi-flip-vertical.png + pentobi-forward.png + pentobi-forward-16.png + pentobi-newgame.png + pentobi-newgame-16.png + pentobi-next-piece.png + pentobi-next-variation.png + pentobi-next-variation-16.png + pentobi-piece-clear.png + pentobi-play.png + pentobi-play-16.png + pentobi-previous-piece.png + pentobi-previous-variation.png + pentobi-previous-variation-16.png + pentobi-rated-game.png + pentobi-rated-game-16.png + pentobi-rotate-left.png + pentobi-rotate-right.png + pentobi-undo.png + pentobi-undo-16.png + ) + +set(pentobi_TS + translations/pentobi.ts + translations/pentobi_de.ts + ) + +# Create PNG icons from SVG icons using the helper program src/convert +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/icons) +file(COPY resources.qrc DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +foreach(icon ${pentobi_ICNS}) + string(REPLACE ".png" ".svg" svgicon ${icon}) + add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${icon}" + COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ${CMAKE_CURRENT_BINARY_DIR}/icons/${icon} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ) +endforeach() +qt5_add_resources(pentobi_RC_SRCS ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc + OPTIONS -no-compress) +file(COPY resources_2x.qrc DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +foreach(icon ${pentobi_ICNS}) +string(REPLACE ".png" ".svg" svgicon ${icon}) +string(REPLACE ".png" "@2x.png" hdpiicon ${icon}) +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon}" + COMMAND convert --hdpi ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} +) +endforeach() +qt5_add_resources(pentobi_RC_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/resources_2x.qrc OPTIONS -no-compress) + +qt5_add_translation(pentobi_QM_SRCS ${pentobi_TS}) + +add_executable(pentobi WIN32 + ${pentobi_SRCS} + ${pentobi_QM_SRCS} + ${pentobi_RC_SRCS} + ) + + +if(MINGW AND (CMAKE_SIZEOF_VOID_P EQUAL "4")) + set_target_properties(pentobi PROPERTIES LINK_FLAGS -Wl,--large-address-aware) +endif() + +target_link_libraries(pentobi + pentobi_gui + pentobi_mcts + pentobi_base + boardgame_base + boardgame_sgf + boardgame_util + boardgame_sys + ) + +target_link_libraries(pentobi Qt5::Widgets Qt5::Concurrent) + +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(pentobi ${CMAKE_THREAD_LIBS_INIT}) +endif() + +install(TARGETS pentobi DESTINATION ${CMAKE_INSTALL_BINDIR}) + +# Install translation files. If you change the destination, you need to +# update the default for PENTOBI_TRANSLATIONS in the main CMakeLists.txt +install(FILES ${pentobi_QM_SRCS} + DESTINATION ${CMAKE_INSTALL_DATADIR}/pentobi/translations) + +install(DIRECTORY help DESTINATION ${CMAKE_INSTALL_DATAROOTDIR} + FILES_MATCHING PATTERN "*.css" PATTERN "*.html" PATTERN "*.png" PATTERN "*.jpg") + +if(MSVC) + configure_file(pentobi.conf.in Debug/pentobi.conf @ONLY) + configure_file(pentobi.conf.in Release/pentobi.conf @ONLY) +else() + configure_file(pentobi.conf.in pentobi.conf @ONLY) +endif() diff --git a/src/pentobi/Main.cpp b/src/pentobi/Main.cpp new file mode 100644 index 0000000..a6b0f92 --- /dev/null +++ b/src/pentobi/Main.cpp @@ -0,0 +1,210 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include "Application.h" +#include "MainWindow.h" + +#ifdef Q_OS_WIN +#include +#include +#include +#include +#endif + +using libboardgame_util::LogInitializer; +using libboardgame_util::RandomGenerator; + +//----------------------------------------------------------------------------- + +namespace { + +void redirectStdErr() +{ +#ifdef Q_OS_WIN + CONSOLE_SCREEN_BUFFER_INFO info; + AllocConsole(); + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info); + info.dwSize.Y = 500; + SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), info.dwSize); + auto stdErrHandle = (intptr_t)GetStdHandle(STD_ERROR_HANDLE); + int conHandle = _open_osfhandle(stdErrHandle, _O_TEXT); + auto f = _fdopen(conHandle, "w"); + *stderr = *f; + setvbuf(stderr, NULL, _IONBF, 0); + ios::sync_with_stdio(); +#endif +} + +} // namespace + +//----------------------------------------------------------------------------- + +int main(int argc, char* argv[]) +{ + LogInitializer log_initializer; + Q_INIT_RESOURCE(libpentobi_gui_resources); +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +#endif + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + Application app(argc, argv); + app.setOrganizationName("Pentobi"); + app.setApplicationName("Pentobi"); + Q_INIT_RESOURCE(libpentobi_gui_resources_2x); + try + { + // For some reason, labels in the status bar have a border on + // Windows 7 with Qt 4.8. We don't want that. + app.setStyleSheet("QStatusBar::item { border: 0px solid black }"); + + // Allow the user to override installation paths with a config file in + // the directory of the executable to test it without installation + QString helpDir; + QString booksDir; + QString translationsPentobiDir; + QString translationsLibPentobiGuiDir; + QString appDir = app.applicationDirPath(); +#ifdef PENTOBI_HELP_DIR + helpDir = PENTOBI_HELP_DIR; +#endif + if (helpDir.isEmpty()) + helpDir = appDir + "/help"; +#ifdef PENTOBI_BOOKS_DIR + booksDir = PENTOBI_BOOKS_DIR; +#endif + if (booksDir.isEmpty()) + booksDir = appDir + "/books"; +#ifdef PENTOBI_TRANSLATIONS + translationsPentobiDir = PENTOBI_TRANSLATIONS; + translationsLibPentobiGuiDir = PENTOBI_TRANSLATIONS; +#endif + if (translationsPentobiDir.isEmpty()) + translationsPentobiDir = appDir + "/translations"; + if (translationsLibPentobiGuiDir.isEmpty()) + translationsLibPentobiGuiDir = appDir + "/translations"; + QString overrideConfigFile = appDir + "/pentobi.conf"; + if (QFileInfo::exists(overrideConfigFile)) + { + QSettings settings(overrideConfigFile, QSettings::IniFormat); + helpDir = settings.value("HelpDir", helpDir).toString(); + booksDir = settings.value("BooksDir", booksDir).toString(); + translationsPentobiDir = + settings.value("TranslationsPentobiDir", + translationsPentobiDir).toString(); + translationsLibPentobiGuiDir = + settings.value("TranslationsLibPentobiGuiDir", + translationsLibPentobiGuiDir).toString(); + } + + QTranslator qtTranslator; + QString qtTranslationPath = + QLibraryInfo::location(QLibraryInfo::TranslationsPath); + QString locale = QLocale::system().name(); + qtTranslator.load("qt_" + locale, qtTranslationPath); + app.installTranslator(&qtTranslator); + QTranslator libPentobiGuiTranslator; + libPentobiGuiTranslator.load("libpentobi_gui_" + locale, + translationsLibPentobiGuiDir); + app.installTranslator(&libPentobiGuiTranslator); + QTranslator pentobiTranslator; + pentobiTranslator.load("pentobi_" + locale, translationsPentobiDir); + app.installTranslator(&pentobiTranslator); + + QCommandLineParser parser; + auto maxSupportedLevel = Player::max_supported_level; + QCommandLineOption optionMaxLevel("maxlevel", + "Set maximum level to .", "n", + QString::number(maxSupportedLevel)); + parser.addOption(optionMaxLevel); + QCommandLineOption optionNoBook("nobook"); + parser.addOption(optionNoBook); + QCommandLineOption optionNoDelay("nodelay"); + parser.addOption(optionNoDelay); + QCommandLineOption optionSeed("seed", "Set random seed to .", "n"); + parser.addOption(optionSeed); + QCommandLineOption optionThreads("threads", "Use threads.", "n"); + parser.addOption(optionThreads); + QCommandLineOption optionVerbose("verbose"); + parser.addOption(optionVerbose); + parser.process(app); + bool ok; + auto maxLevel = parser.value(optionMaxLevel).toUInt(&ok); + if (! ok || maxLevel < 1 || maxLevel > maxSupportedLevel) + throw runtime_error("--maxlevel must be between 1 and " + + to_string(maxSupportedLevel)); + unsigned threads = 0; + if (parser.isSet(optionThreads)) + { + threads = parser.value(optionThreads).toUInt(&ok); + if (! ok || threads == 0) + throw runtime_error("--threads must be greater zero."); + if (! libpentobi_mcts::SearchParamConst::multithread + && threads > 1) + throw runtime_error("This version of Pentobi was compiled" + " without support for multi-threading."); + } + if (! parser.isSet(optionVerbose)) + libboardgame_util::disable_logging(); + else + redirectStdErr(); + if (parser.isSet(optionSeed)) + { + RandomGenerator::ResultType seed = + parser.value(optionSeed).toUInt(&ok); + if (! ok) + throw runtime_error("--seed must be a positive number"); + RandomGenerator::set_global_seed(seed); + } + bool noBook = parser.isSet(optionNoBook); + QString initialFile; + auto args = parser.positionalArguments(); + if (! args.empty()) + initialFile = args.at(0); + QSettings settings; + auto variantString = settings.value("variant", "").toString(); + Variant variant; + if (! parse_variant_id(variantString.toLocal8Bit().constData(), + variant)) + variant = Variant::duo; + try + { + MainWindow mainWindow(variant, initialFile, helpDir, maxLevel, + booksDir, noBook, threads); + if (parser.isSet(optionNoDelay)) + mainWindow.setNoDelay(); + mainWindow.show(); + return app.exec(); + } + catch (bad_alloc&) + { + // bad_alloc is an expected error because libpentobi_mcts::Player + // requires a larger amount of memory and an error message should + // be shown to the user. It needs to be handled here because it + // needs the translators being installed for the error message. + QMessageBox::critical( + nullptr, app.translate("main", "Pentobi"), + app.translate("main", "Not enough memory.")); + } + } + catch (const exception& e) + { + cerr << "Error: " << e.what() << '\n'; + return 1; + } +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/MainWindow.cpp b/src/pentobi/MainWindow.cpp new file mode 100644 index 0000000..b74b6eb --- /dev/null +++ b/src/pentobi/MainWindow.cpp @@ -0,0 +1,3466 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/MainWindow.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "MainWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "AnalyzeGameWindow.h" +#include "AnalyzeSpeedDialog.h" +#include "RatingDialog.h" +#include "ShowMessage.h" +#include "Util.h" +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_util/Assert.h" +#include "libpentobi_base/TreeUtil.h" +#include "libpentobi_base/PentobiTreeWriter.h" +#include "libpentobi_gui/ComputerColorDialog.h" +#include "libpentobi_gui/GameInfoDialog.h" +#include "libpentobi_gui/GuiBoard.h" +#include "libpentobi_gui/GuiBoardUtil.h" +#include "libpentobi_gui/HelpWindow.h" +#include "libpentobi_gui/InitialRatingDialog.h" +#include "libpentobi_gui/LeaveFullscreenButton.h" +#include "libpentobi_gui/OrientationDisplay.h" +#include "libpentobi_gui/PieceSelector.h" +#include "libpentobi_gui/SameHeightLayout.h" +#include "libpentobi_gui/ScoreDisplay.h" +#include "libpentobi_gui/Util.h" + +using Util::getPlayerString; +using libboardgame_sgf::InvalidTree; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::back_to_main_variation; +using libboardgame_sgf::util::beginning_of_branch; +using libboardgame_sgf::util::find_next_comment; +using libboardgame_sgf::util::get_last_node; +using libboardgame_sgf::util::get_move_annotation; +using libboardgame_sgf::util::get_variation_string; +using libboardgame_sgf::util::has_comment; +using libboardgame_sgf::util::has_earlier_variation; +using libboardgame_sgf::util::is_main_variation; +using libboardgame_util::clear_abort; +using libboardgame_util::get_abort; +using libboardgame_util::set_abort; +using libboardgame_util::trim_right; +using libboardgame_util::ArrayList; +using libpentobi_base::BoardType; +using libpentobi_base::MoveInfo; +using libpentobi_base::MoveInfoExt; +using libpentobi_base::PieceInfo; +using libpentobi_base::PieceSet; +using libpentobi_base::PentobiTree; +using libpentobi_base::PentobiTreeWriter; +using libpentobi_base::ScoreType; +using libpentobi_base::tree_util::get_move_number; +using libpentobi_base::tree_util::get_moves_left; +using libpentobi_base::tree_util::get_position_info; +using libpentobi_mcts::Search; + +//----------------------------------------------------------------------------- + +namespace { + +/** Create a button for manipulating piece orientation. */ +QToolButton* createOBoxToolButton(QAction* action) +{ + auto button = new QToolButton; + button->setDefaultAction(action); + button->setAutoRaise(true); + // No focus, there are faster keyboard shortcuts for manipulating pieces + button->setFocusPolicy(Qt::NoFocus); + // For some reason, toolbuttons are very small in Ubuntu Unity if outside + // a toolbar (tested with Ubuntu 15.10) + button->setMinimumSize(28, 28); + return button; +} + +/** Return auto-save file name as a native path name. */ +QString getAutoSaveFile() +{ + return Util::getDataDir() + QDir::separator() + "autosave.blksgf"; +} + +bool hasCurrentVariationOtherMoves(const PentobiTree& tree, + const SgfNode& current) +{ + auto node = current.get_parent_or_null(); + while (node) + { + if (! tree.get_move(*node).is_null()) + return true; + node = node->get_parent_or_null(); + } + node = current.get_first_child_or_null(); + while (node) + { + if (! tree.get_move(*node).is_null()) + return true; + node = node->get_first_child_or_null(); + } + return false; +} + +void initToolBarText(QToolBar* toolBar) +{ + QSettings settings; + auto toolBarText = settings.value("toolbar_text", "system").toString(); + if (toolBarText == "no_text") + toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); + else if (toolBarText == "beside_icons") + toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + else if (toolBarText == "below_icons") + toolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + else if (toolBarText == "text_only") + toolBar->setToolButtonStyle(Qt::ToolButtonTextOnly); + else + toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); +} + +void setIcon(QAction* action, const QString& name) +{ + QIcon icon(QString(":/pentobi/icons/%1.png").arg(name)); + QString file16 = QString(":/pentobi/icons/%1-16.png").arg(name); + if (QFile::exists(file16)) + icon.addFile(file16, QSize(16, 16)); + action->setIcon(icon); +} + +/** Simple heuristic that prefers moves with more score points. + Used for sorting the list used in Find Move. */ +ScoreType getHeuristic(const Board& bd, Move mv) +{ + return bd.get_piece_info(bd.get_move_piece(mv)).get_score_points(); +} + +} // namespace + +//----------------------------------------------------------------------------- + +MainWindow::MainWindow(Variant variant, const QString& initialFile, + const QString& helpDir, unsigned maxLevel, + const QString& booksDir, bool noBook, + unsigned nuThreads) + : m_game(variant), + m_bd(m_game.get_board()), + m_helpDir(helpDir) +{ + if (maxLevel > Player::max_supported_level) + maxLevel = Player::max_supported_level; + if (maxLevel < 1) + maxLevel = 1; + m_maxLevel = maxLevel; + Util::initDataDir(); + QSettings settings; + m_history.reset(new RatingHistory(variant)); + createActions(); + restoreLevel(variant); + setCentralWidget(createCentralWidget()); + initPieceSelectors(); + m_moveNumber = new QLabel; + statusBar()->addPermanentWidget(m_moveNumber); + m_setupModeLabel = new QLabel(tr("Setup mode")); + statusBar()->addWidget(m_setupModeLabel); + m_setupModeLabel->hide(); + m_ratedGameLabelText = new QLabel(tr("Rated game")); + statusBar()->addWidget(m_ratedGameLabelText); + m_ratedGameLabelText->hide(); + initGame(); + m_player.reset(new Player(variant, maxLevel, + booksDir.toLocal8Bit().constData(), nuThreads)); + m_player->get_search().set_callback(bind(&MainWindow::searchCallback, + this, placeholders::_1, + placeholders::_2)); + m_player->set_use_book(! noBook); + createToolBar(); + connect(&m_genMoveWatcher, SIGNAL(finished()), + SLOT(genMoveFinished())); + connect(m_guiBoard, SIGNAL(play(Color, Move)), + SLOT(placePiece(Color, Move))); + connect(m_guiBoard, SIGNAL(pointClicked(Point)), + SLOT(pointClicked(Point))); + connect(m_actionMovePieceLeft, SIGNAL(triggered()), + m_guiBoard, SLOT(movePieceLeft())); + connect(m_actionMovePieceRight, SIGNAL(triggered()), + m_guiBoard, SLOT(movePieceRight())); + connect(m_actionMovePieceUp, SIGNAL(triggered()), + m_guiBoard, SLOT(movePieceUp())); + connect(m_actionMovePieceDown, SIGNAL(triggered()), + m_guiBoard, SLOT(movePieceDown())); + connect(m_actionPlacePiece, SIGNAL(triggered()), + m_guiBoard, SLOT(placePiece())); + createMenu(); + qApp->installEventFilter(this); + updateRecentFiles(); + auto marking = settings.value("move_marking", "last_dot").toString(); + if (marking == "all_number") + m_actionMoveMarkingAllNumber->setChecked(true); + else if (marking == "last_dot") + m_actionMoveMarkingLastDot->setChecked(true); + else if (marking == "last_number") + m_actionMoveMarkingLastNumber->setChecked(true); + else + m_actionMoveMarkingNone->setChecked(true); + auto coordinates = settings.value("coordinates", false).toBool(); + m_guiBoard->setCoordinates(coordinates); + m_actionCoordinates->setChecked(coordinates); + auto showToolbar = settings.value("toolbar", true).toBool(); + findChild()->setVisible(showToolbar); + m_menuToolBarText->setEnabled(showToolbar); + m_actionShowToolbar->setChecked(showToolbar); + auto showVariations = settings.value("show_variations", true).toBool(); + m_actionShowVariations->setChecked(showVariations); + initVariantActions(); + QIcon icon; + icon.addFile(":/pentobi/icons/pentobi.png"); + icon.addFile(":/pentobi/icons/pentobi-16.png"); + icon.addFile(":/pentobi/icons/pentobi-32.png"); + setWindowIcon(icon); + + bool centerOnScreen = false; + QRect screenGeometry = QApplication::desktop()->screenGeometry(); + if (restoreGeometry(settings.value("geometry").toByteArray())) + { + if (! screenGeometry.contains(geometry())) + { + if (width() > screenGeometry.width() + || height() > screenGeometry.height()) + adjustSize(); + centerOnScreen = true; + } + } + else + { + adjustSize(); + centerOnScreen = true; + } + if (centerOnScreen) + { + int x = (screenGeometry.width() - width()) / 2; + int y = (screenGeometry.height() - height()) / 2; + move(x, y); + } + + auto showComment = settings.value("show_comment", false).toBool(); + m_comment->setVisible(showComment); + if (showComment) + m_splitter->restoreState( + settings.value("splitter_state").toByteArray()); + m_actionShowComment->setChecked(showComment); + updateWindow(true); + clearFile(); + if (! initialFile.isEmpty()) + { + if (open(initialFile)) + rememberFile(initialFile); + } + else + { + QString autoSaveFile = getAutoSaveFile(); + if (QFile(autoSaveFile).exists()) + { + open(autoSaveFile, true); + m_isAutoSaveLoaded = true; + deleteAutoSaveFile(); + m_gameFinished = m_bd.is_game_over(); + updateWindow(true); + if (settings.value("autosave_rated", false).toBool()) + QMetaObject::invokeMethod(this, "continueRatedGame", + Qt::QueuedConnection); + } + } +} + +MainWindow::~MainWindow() +{ +} + +void MainWindow::about() +{ + QMessageBox::about(this, tr("About Pentobi"), + "" + "

" + tr("Pentobi") + "

" + "

" + tr("Version %1").arg(getVersion()) + "

" + "

" + + tr("Computer opponent for the board game Blokus.") + + "
" + + tr("© 2011–%1 Markus Enzenberger").arg(2017) + + + "
" + + "http://pentobi.sourceforge.net" + "

"); +} + +void MainWindow::analyzeGame() +{ + if (! is_main_variation(m_game.get_current())) + { + showInfo(tr("Game analysis is only possible in the main variation.")); + return; + } + AnalyzeSpeedDialog dialog(this, tr("Analyze Game")); + if (! dialog.exec()) + return; + int speed = dialog.getSpeedValue(); + cancelThread(); + if (m_analyzeGameWindow) + delete m_analyzeGameWindow; + m_analyzeGameWindow = new AnalyzeGameWindow(this); + // Make sure all action shortcuts work when the analyze dialog has the + // focus apart from m_actionLeaveFullscreen because the Esc key is used + // to close the dialog + m_analyzeGameWindow->addActions(actions()); + m_analyzeGameWindow->removeAction(m_actionLeaveFullscreen); + m_analyzeGameWindow->show(); + m_isAnalyzeRunning = true; + connect(m_analyzeGameWindow->analyzeGameWidget, SIGNAL(finished()), + SLOT(analyzeGameFinished())); + connect(m_analyzeGameWindow->analyzeGameWidget, + SIGNAL(gotoPosition(Variant,const vector&)), + SLOT(gotoPosition(Variant,const vector&))); + size_t nuSimulations; + switch (speed) + { + case 0: + nuSimulations = 6000; + break; + case 1: + nuSimulations = 24000; + break; + default: + nuSimulations = 96000; + } + m_analyzeGameWindow->analyzeGameWidget->start( + m_game, m_player->get_search(), nuSimulations); +} + +void MainWindow::analyzeGameFinished() +{ + m_analyzeGameWindow->analyzeGameWidget->setCurrentPosition( + m_game, m_game.get_current()); + m_isAnalyzeRunning = false; +} + +/** Call to Player::genmove() that runs in a different thread. */ +MainWindow::GenMoveResult MainWindow::asyncGenMove(Color c, int genMoveId, + bool playSingleMove) +{ + QElapsedTimer timer; + timer.start(); + GenMoveResult result; + result.playSingleMove = playSingleMove; + result.color = c; + result.genMoveId = genMoveId; + result.move = m_player->genmove(m_bd, c); + auto elapsed = timer.elapsed(); + // Enforce minimum thinking time of 0.8 sec + if (elapsed < 800 && ! m_noDelay) + QThread::msleep(800 - elapsed); + return result; +} + +void MainWindow::badMove(bool checked) +{ + if (! checked) + return; + m_game.set_bad_move(); + updateWindow(false); +} + +void MainWindow::backward() +{ + gotoNode(m_game.get_current().get_parent_or_null()); +} + +void MainWindow::backToMainVariation() +{ + gotoNode(back_to_main_variation(m_game.get_current())); +} + +void MainWindow::beginning() +{ + gotoNode(m_game.get_root()); +} + +void MainWindow::beginningOfBranch() +{ + gotoNode(beginning_of_branch(m_game.get_current())); +} + +void MainWindow::cancelThread() +{ + if (m_isAnalyzeRunning) + { + // This should never happen because AnalyzeGameWindow protects the + // parent with a modal progress dialog while it is running. However, + // due to bugs in Unity 2D (tested with Ubuntu 11.04 and 11.10), the + // global menu can still trigger menu item events. + m_analyzeGameWindow->analyzeGameWidget->cancel(); + } + if (! m_isGenMoveRunning) + return; + // After waitForFinished() returns, we can be sure that the move generation + // is no longer running, but we will still receive the finished event. + // Increasing m_genMoveId will make genMoveFinished() ignore the event. + ++m_genMoveId; + set_abort(); + m_genMoveWatcher.waitForFinished(); + m_isGenMoveRunning = false; + clearStatus(); + setCursor(QCursor(Qt::ArrowCursor)); + m_actionInterrupt->setEnabled(false); + m_actionNextPiece->setEnabled(true); + m_actionPlay->setEnabled(true); + m_actionPlaySingleMove->setEnabled(true); + m_actionPreviousPiece->setEnabled(true); +} + +void MainWindow::checkComputerMove() +{ + if (m_autoPlay && isComputerToPlay() && ! m_bd.is_game_over() + && ! m_isGenMoveRunning) + genMove(); +} + +bool MainWindow::checkSave() +{ + if (! m_file.isEmpty()) + { + if (! m_game.is_modified()) + return true; + QMessageBox msgBox(this); + initQuestion(msgBox, tr("The file has been modified."), + tr("Do you want to save your changes?")); + // Don't use QMessageBox::Discard because on some platforms it uses the + // text "Close without saving" which implies that the window would be + // closed + auto discardButton = + msgBox.addButton(tr("&Don't Save"), QMessageBox::DestructiveRole); + auto saveButton = msgBox.addButton(QMessageBox::Save); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + auto result = msgBox.clickedButton(); + if (result == saveButton) + { + save(); + return true; + } + return result == discardButton; + } + // Don't ask if game should be saved if it was finished because the user + // might only want to play and never save games. + if (m_game.get_root().has_children() && ! m_gameFinished) + { + QMessageBox msgBox(this); + initQuestion(msgBox, tr("The current game is not finished."), + tr("Do you want to abort the game?")); + auto abortGameButton = + msgBox.addButton(tr("&Abort Game"), QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != abortGameButton) + return false; + return true; + } + return true; +} + +bool MainWindow::checkQuit() +{ + if (! m_file.isEmpty() && m_game.is_modified()) + { + QMessageBox msgBox(this); + initQuestion(msgBox, tr("The file has been modified."), + tr("Do you want to save your changes?")); + auto discardButton = msgBox.addButton(QMessageBox::Discard); + auto saveButton = msgBox.addButton(QMessageBox::Save); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + auto result = msgBox.clickedButton(); + if (result == saveButton) + { + save(); + return true; + } + return result == discardButton; + } + cancelThread(); + QSettings settings; + if (m_file.isEmpty() && ! m_gameFinished + && (m_game.is_modified() || m_isAutoSaveLoaded)) + { + writeGame(getAutoSaveFile().toLocal8Bit().constData()); + settings.setValue("autosave_rated", m_isRated); + if (m_isRated) + settings.setValue("autosave_rated_color", + m_ratedGameColor.to_int()); + } + if (! isFullScreen()) + settings.setValue("geometry", saveGeometry()); + if (m_comment->isVisible()) + settings.setValue("splitter_state", m_splitter->saveState()); + return true; +} + +void MainWindow::clearFile() +{ + setFile(""); +} + +void MainWindow::clearPiece() +{ + m_actionRotateClockwise->setEnabled(false); + m_actionRotateAnticlockwise->setEnabled(false); + m_actionFlipHorizontally->setEnabled(false); + m_actionFlipVertically->setEnabled(false); + m_actionClearPiece->setEnabled(false); + m_guiBoard->clearPiece(); + m_orientationDisplay->clearPiece(); +} + +void MainWindow::clearStatus() +{ + statusBar()->clearMessage(); +} + +void MainWindow::closeEvent(QCloseEvent* event) +{ + if (checkQuit()) + event->accept(); + else + event->ignore(); +} + +void MainWindow::commentChanged() +{ + if (m_ignoreCommentTextChanged) + return; + QString comment = m_comment->toPlainText(); + if (comment.isEmpty()) + m_game.set_comment(""); + else + { + string charset = m_game.get_root().get_property("CA", ""); + string value = Util::convertSgfValueFromQString(comment, charset); + value = trim_right(value); + m_game.set_comment(value); + } + updateWindow(false); +} + +void MainWindow::computerColors() +{ + ColorMap oldComputerColors = m_computerColors; + ComputerColorDialog dialog(this, m_bd.get_variant(), m_computerColors); + dialog.exec(); + auto nu_colors = m_bd.get_nu_nonalt_colors(); + + bool computerNone = true; + for (Color c : Color::Range(nu_colors)) + if (m_computerColors[c]) + { + computerNone = false; + break; + } + QSettings settings; + settings.setValue("computer_color_none", computerNone); + + // Enable autoplay only if any color has changed because that means that + // the user probably wants to continue playing, otherwise the user could + // have only opened the dialog to check the current settings + for (Color c : Color::Range(nu_colors)) + if (m_computerColors[c] != oldComputerColors[c]) + { + m_autoPlay = true; + break; + } + + checkComputerMove(); +} + +bool MainWindow::computerPlaysAll() const +{ + for (Color c : Color::Range(m_bd.get_nu_nonalt_colors())) + if (! m_computerColors[c]) + return false; + return true; +} + +void MainWindow::continueRatedGame() +{ + auto nuColors = m_bd.get_nu_colors(); + QSettings settings; + auto color = + static_cast( + settings.value("autosave_rated_color", 0).toUInt()); + if (color >= nuColors) + return; + m_ratedGameColor = Color(color); + m_computerColors.fill(true); + for (Color c : Color::Range(nuColors)) + if (m_bd.is_same_player(c, m_ratedGameColor)) + m_computerColors[c] = false; + setRated(true); + updateWindow(false); + showInfo(tr("Continuing unfinished rated game."), + tr("You play %1 in this game.") + .arg(getPlayerString(m_bd.get_variant(), m_ratedGameColor))); + m_autoPlay = true; + checkComputerMove(); +} + +void MainWindow::coordinates(bool checked) +{ + m_guiBoard->setCoordinates(checked); + QSettings settings; + settings.setValue("coordinates", checked); +} + +QAction* MainWindow::createAction(const QString& text) +{ + auto action = new QAction(text, this); + // Add all actions also to main window. if an action is only added to + // the menu bar, shortcuts won't work in fullscreen mode because the menu + // is not visible in fullscreen mode + addAction(action); + return action; +} + +QAction* MainWindow::createActionLevel(unsigned level, const QString& text) +{ + auto action = createAction(text); + action->setCheckable(true); + action->setActionGroup(m_actionGroupLevel); + action->setData(level); + connect(action, SIGNAL(triggered(bool)), SLOT(levelTriggered(bool))); + return action; +} + +void MainWindow::createActions() +{ + m_actionGroupVariant = new QActionGroup(this); + m_actionGroupLevel = new QActionGroup(this); + auto groupMoveMarking = new QActionGroup(this); + auto groupMoveAnnotation = new QActionGroup(this); + auto groupToolBarText = new QActionGroup(this); + + m_actionAbout = createAction(tr("&About Pentobi")); + connect(m_actionAbout, SIGNAL(triggered()), SLOT(about())); + + m_actionAnalyzeGame = createAction(tr("&Analyze Game...")); + connect(m_actionAnalyzeGame, SIGNAL(triggered()), SLOT(analyzeGame())); + + m_actionBackward = createAction(tr("B&ackward")); + m_actionBackward->setToolTip(tr("Go one move backward")); + m_actionBackward->setPriority(QAction::LowPriority); + setIcon(m_actionBackward, "pentobi-backward"); + m_actionBackward->setShortcut(QKeySequence::MoveToPreviousWord); + connect(m_actionBackward, SIGNAL(triggered()), SLOT(backward())); + + m_actionBackToMainVariation = createAction(tr("Back to &Main Variation")); + m_actionBackToMainVariation->setShortcut(QString("Ctrl+M")); + connect(m_actionBackToMainVariation, SIGNAL(triggered()), + SLOT(backToMainVariation())); + + m_actionBadMove = createAction(tr("&Bad")); + m_actionBadMove->setActionGroup(groupMoveAnnotation); + m_actionBadMove->setCheckable(true); + connect(m_actionBadMove, SIGNAL(triggered(bool)), SLOT(badMove(bool))); + + m_actionBeginning = createAction(tr("&Beginning")); + m_actionBeginning->setToolTip(tr("Go to beginning of game")); + m_actionBeginning->setPriority(QAction::LowPriority); + setIcon(m_actionBeginning, "pentobi-beginning"); + m_actionBeginning->setShortcut(QKeySequence::MoveToStartOfDocument); + connect(m_actionBeginning, SIGNAL(triggered()), SLOT(beginning())); + + m_actionBeginningOfBranch = createAction(tr("Beginning of Bran&ch")); + m_actionBeginningOfBranch->setShortcut(QString("Ctrl+B")); + connect(m_actionBeginningOfBranch, SIGNAL(triggered()), + SLOT(beginningOfBranch())); + + m_actionClearPiece = createAction(tr("Clear Piece")); + setIcon(m_actionClearPiece, "pentobi-piece-clear"); + m_actionClearPiece->setShortcut(QString("0")); + connect(m_actionClearPiece, SIGNAL(triggered()), SLOT(clearPiece())); + + m_actionComputerColors = createAction(tr("&Computer Colors")); + m_actionComputerColors->setShortcut(QString("Ctrl+U")); + m_actionComputerColors->setToolTip( + tr("Set the colors played by the computer")); + setIcon(m_actionComputerColors, "pentobi-computer-colors"); + connect(m_actionComputerColors, SIGNAL(triggered()), + SLOT(computerColors())); + + m_actionCoordinates = createAction(tr("C&oordinates")); + m_actionCoordinates->setCheckable(true); + connect(m_actionCoordinates, SIGNAL(triggered(bool)), + SLOT(coordinates(bool))); + + m_actionDeleteAllVariations = createAction(tr("&Delete All Variations")); + connect(m_actionDeleteAllVariations, SIGNAL(triggered()), + SLOT(deleteAllVariations())); + + m_actionDoubtfulMove = createAction(tr("&Doubtful")); + m_actionDoubtfulMove->setActionGroup(groupMoveAnnotation); + m_actionDoubtfulMove->setCheckable(true); + connect(m_actionDoubtfulMove, SIGNAL(triggered(bool)), + SLOT(doubtfulMove(bool))); + + m_actionEnd = createAction(tr("&End")); + m_actionEnd->setToolTip(tr("Go to end of moves")); + m_actionEnd->setPriority(QAction::LowPriority); + m_actionEnd->setShortcut(QKeySequence::MoveToEndOfDocument); + setIcon(m_actionEnd, "pentobi-end"); + connect(m_actionEnd, SIGNAL(triggered()), SLOT(end())); + + m_actionExportAsciiArt = createAction(tr("&ASCII Art")); + connect(m_actionExportAsciiArt, SIGNAL(triggered()), + SLOT(exportAsciiArt())); + + m_actionExportImage = createAction(tr("I&mage")); + connect(m_actionExportImage, SIGNAL(triggered()), SLOT(exportImage())); + + m_actionFindMove = createAction(tr("&Find Move")); + m_actionFindMove->setShortcut(QString("F6")); + connect(m_actionFindMove, SIGNAL(triggered()), SLOT(findMove())); + + m_actionFindNextComment = createAction(tr("Find Next &Comment")); + m_actionFindNextComment->setShortcut(QString("F3")); + connect(m_actionFindNextComment, SIGNAL(triggered()), + SLOT(findNextComment())); + + m_actionFlipHorizontally = createAction(tr("Flip Horizontally")); + setIcon(m_actionFlipHorizontally, "pentobi-flip-horizontal"); + connect(m_actionFlipHorizontally, SIGNAL(triggered()), + SLOT(flipHorizontally())); + + m_actionFlipVertically = createAction(tr("Flip Vertically")); + setIcon(m_actionFlipVertically, "pentobi-flip-vertical"); + + m_actionForward = createAction(tr("&Forward")); + m_actionForward->setToolTip(tr("Go one move forward")); + m_actionForward->setPriority(QAction::LowPriority); + m_actionForward->setShortcut(QKeySequence::MoveToNextWord); + setIcon(m_actionForward, "pentobi-forward"); + connect(m_actionForward, SIGNAL(triggered()), SLOT(forward())); + + m_actionFullscreen = createAction(tr("&Fullscreen")); + // Don't use QKeySequence::Fullscreen, it is Ctrl-F11 on Linux but that + // doesn't work in Xfce + m_actionFullscreen->setShortcut(QString("F11")); + connect(m_actionFullscreen, SIGNAL(triggered()), SLOT(fullscreen())); + + m_actionGameInfo = createAction(tr("Ga&me Info")); + m_actionGameInfo->setShortcut(QString("Ctrl+I")); + connect(m_actionGameInfo, SIGNAL(triggered()), SLOT(gameInfo())); + + m_actionGoodMove = createAction(tr("&Good")); + m_actionGoodMove->setActionGroup(groupMoveAnnotation); + m_actionGoodMove->setCheckable(true); + connect(m_actionGoodMove, SIGNAL(triggered(bool)), SLOT(goodMove(bool))); + + m_actionGotoMove = createAction(tr("&Go to Move...")); + m_actionGotoMove->setShortcut(QString("Ctrl+G")); + connect(m_actionGotoMove, SIGNAL(triggered()), SLOT(gotoMove())); + + m_actionHelp = createAction(tr("Pentobi &Help")); + m_actionHelp->setShortcut(QKeySequence::HelpContents); + connect(m_actionHelp, SIGNAL(triggered()), SLOT(help())); + + m_actionInterestingMove = createAction(tr("I&nteresting")); + m_actionInterestingMove->setActionGroup(groupMoveAnnotation); + m_actionInterestingMove->setCheckable(true); + connect(m_actionInterestingMove, SIGNAL(triggered(bool)), + SLOT(interestingMove(bool))); + + m_actionInterrupt = createAction(tr("St&op")); + m_actionInterrupt->setEnabled(false); + connect(m_actionInterrupt, SIGNAL(triggered()), SLOT(interrupt())); + + m_actionInterruptPlay = createAction(); + m_actionInterruptPlay->setShortcut(QString("Shift+Esc")); + connect(m_actionInterruptPlay, SIGNAL(triggered()), SLOT(interruptPlay())); + + m_actionKeepOnlyPosition = createAction(tr("&Keep Only Position")); + connect(m_actionKeepOnlyPosition, SIGNAL(triggered()), + SLOT(keepOnlyPosition())); + + m_actionKeepOnlySubtree = createAction(tr("Keep Only &Subtree")); + connect(m_actionKeepOnlySubtree, SIGNAL(triggered()), + SLOT(keepOnlySubtree())); + + m_actionLeaveFullscreen = createAction(tr("Leave Fullscreen")); + m_actionLeaveFullscreen->setShortcut(QString("Esc")); + connect(m_actionLeaveFullscreen, SIGNAL(triggered()), + SLOT(leaveFullscreen())); + + m_actionMakeMainVariation = createAction(tr("M&ake Main Variation")); + connect(m_actionMakeMainVariation, SIGNAL(triggered()), + SLOT(makeMainVariation())); + + m_actionMoveDownVariation = createAction(tr("Move Variation D&own")); + connect(m_actionMoveDownVariation, SIGNAL(triggered()), + SLOT(moveDownVariation())); + + m_actionMoveUpVariation = createAction(tr("Move Variation &Up")); + connect(m_actionMoveUpVariation, SIGNAL(triggered()), + SLOT(moveUpVariation())); + + static_assert(Player::max_supported_level <= 9, ""); + QString levelText[Player::max_supported_level] = + { tr("&1"), tr("&2"), tr("&3"), tr("&4"), tr("&5"), tr("&6"), + tr("&7"), tr("&8"), tr("&9") }; + for (unsigned i = 0; i < m_maxLevel; ++i) + createActionLevel(i + 1, levelText[i]); + connect(m_actionFlipVertically, SIGNAL(triggered()), + SLOT(flipVertically())); + + m_actionMoveMarkingAllNumber = createAction(tr("&All with Number")); + m_actionMoveMarkingAllNumber->setActionGroup(groupMoveMarking); + m_actionMoveMarkingAllNumber->setCheckable(true); + connect(m_actionMoveMarkingAllNumber, SIGNAL(triggered(bool)), + SLOT(setMoveMarkingAllNumber(bool))); + + m_actionMoveMarkingLastDot = createAction(tr("Last with &Dot")); + m_actionMoveMarkingLastDot->setActionGroup(groupMoveMarking); + m_actionMoveMarkingLastDot->setCheckable(true); + m_actionMoveMarkingLastDot->setChecked(true); + connect(m_actionMoveMarkingLastDot, SIGNAL(triggered(bool)), + SLOT(setMoveMarkingLastDot(bool))); + + m_actionMoveMarkingLastNumber = createAction(tr("&Last with Number")); + m_actionMoveMarkingLastNumber->setActionGroup(groupMoveMarking); + m_actionMoveMarkingLastNumber->setCheckable(true); + m_actionMoveMarkingLastNumber->setChecked(true); + connect(m_actionMoveMarkingLastNumber, SIGNAL(triggered(bool)), + SLOT(setMoveMarkingLastNumber(bool))); + + m_actionMoveMarkingNone = createAction(tr("&None", "move numbers")); + m_actionMoveMarkingNone->setActionGroup(groupMoveMarking); + m_actionMoveMarkingNone->setCheckable(true); + connect(m_actionMoveMarkingNone, SIGNAL(triggered(bool)), + SLOT(setMoveMarkingNone(bool))); + + m_actionMovePieceLeft = createAction(); + m_actionMovePieceLeft->setShortcut(QKeySequence::MoveToPreviousChar); + + m_actionMovePieceRight = createAction(); + m_actionMovePieceRight->setShortcut(QKeySequence::MoveToNextChar); + + m_actionMovePieceUp = createAction(); + m_actionMovePieceUp->setShortcut(QKeySequence::MoveToPreviousLine); + + m_actionMovePieceDown = createAction(); + m_actionMovePieceDown->setShortcut(QKeySequence::MoveToNextLine); + + m_actionNextPiece = createAction(tr("Next Piece")); + setIcon(m_actionNextPiece, "pentobi-next-piece"); + m_actionNextPiece->setShortcut(QString("+")); + connect(m_actionNextPiece, SIGNAL(triggered()), SLOT(nextPiece())); + + m_actionNextTransform = createAction(); + m_actionNextTransform->setShortcut(QString("Space")); + connect(m_actionNextTransform, SIGNAL(triggered()), SLOT(nextTransform())); + + m_actionNextVariation = createAction(tr("&Next Variation")); + m_actionNextVariation->setToolTip(tr("Go to next variation")); + m_actionNextVariation->setPriority(QAction::LowPriority); + m_actionNextVariation->setShortcut(QKeySequence::MoveToNextPage); + setIcon(m_actionNextVariation, "pentobi-next-variation"); + connect(m_actionNextVariation, SIGNAL(triggered()), SLOT(nextVariation())); + + m_actionNew = createAction(tr("&New")); + m_actionNew->setShortcut(QKeySequence::New); + m_actionNew->setToolTip(tr("Start a new game")); + setIcon(m_actionNew, "pentobi-newgame"); + connect(m_actionNew, SIGNAL(triggered()), SLOT(newGame())); + + m_actionNoMoveAnnotation = createAction(tr("N&one", "move annotation")); + m_actionNoMoveAnnotation->setActionGroup(groupMoveAnnotation); + m_actionNoMoveAnnotation->setCheckable(true); + connect(m_actionNoMoveAnnotation, SIGNAL(triggered(bool)), + SLOT(noMoveAnnotation(bool))); + + m_actionOpen = createAction(tr("&Open...")); + m_actionOpen->setShortcut(QKeySequence::Open); + connect(m_actionOpen, SIGNAL(triggered()), SLOT(open())); + m_actionPlacePiece = createAction(); + m_actionPlacePiece->setShortcut(QString("Return")); + + m_actionPlay = createAction(tr("&Play")); + m_actionPlay->setShortcut(QString("Ctrl+L")); + setIcon(m_actionPlay, "pentobi-play"); + connect(m_actionPlay, SIGNAL(triggered()), SLOT(play())); + + m_actionPlaySingleMove = createAction(tr("Play &Single Move")); + m_actionPlaySingleMove->setShortcut(QString("Ctrl+Shift+L")); + connect(m_actionPlaySingleMove, SIGNAL(triggered()), + SLOT(playSingleMove())); + + m_actionPreviousPiece = createAction(tr("Previous Piece")); + setIcon(m_actionPreviousPiece, "pentobi-previous-piece"); + m_actionPreviousPiece->setShortcut(QString("-")); + connect(m_actionPreviousPiece, SIGNAL(triggered()), + SLOT(previousPiece())); + + m_actionPreviousTransform = createAction(); + m_actionPreviousTransform->setShortcut(QString("Shift+Space")); + connect(m_actionPreviousTransform, SIGNAL(triggered()), + SLOT(previousTransform())); + + m_actionPreviousVariation = createAction(tr("&Previous Variation")); + m_actionPreviousVariation->setToolTip(tr("Go to previous variation")); + m_actionPreviousVariation->setPriority(QAction::LowPriority); + m_actionPreviousVariation->setShortcut(QKeySequence::MoveToPreviousPage); + setIcon(m_actionPreviousVariation, "pentobi-previous-variation"); + connect(m_actionPreviousVariation, SIGNAL(triggered()), + SLOT(previousVariation())); + + m_actionRatedGame = createAction(tr("&Rated Game")); + m_actionRatedGame->setToolTip(tr("Start a rated game")); + m_actionRatedGame->setShortcut(QString("Ctrl+Shift+N")); + setIcon(m_actionRatedGame, "pentobi-rated-game"); + connect(m_actionRatedGame, SIGNAL(triggered()), SLOT(ratedGame())); + + for (auto& action : m_actionRecentFile) + { + action = createAction(); + action->setVisible(false); + connect(action, SIGNAL(triggered()), SLOT(openRecentFile())); + } + + m_actionRotateAnticlockwise = createAction(tr("Rotate Anticlockwise")); + setIcon(m_actionRotateAnticlockwise, "pentobi-rotate-left"); + connect(m_actionRotateAnticlockwise, SIGNAL(triggered()), + SLOT(rotateAnticlockwise())); + + m_actionRotateClockwise = createAction(tr("Rotate Clockwise")); + setIcon(m_actionRotateClockwise, "pentobi-rotate-right"); + connect(m_actionRotateClockwise, SIGNAL(triggered()), + SLOT(rotateClockwise())); + + m_actionQuit = createAction(tr("&Quit")); + m_actionQuit->setShortcut(QKeySequence::Quit); + connect(m_actionQuit, SIGNAL(triggered()), SLOT(close())); + + m_actionSave = createAction(tr("&Save")); + m_actionSave->setShortcut(QKeySequence::Save); + connect(m_actionSave, SIGNAL(triggered()), SLOT(save())); + + m_actionSaveAs = createAction(tr("Save &As...")); + m_actionSaveAs->setShortcut(QKeySequence::SaveAs); + connect(m_actionSaveAs, SIGNAL(triggered()), SLOT(saveAs())); + + m_actionNextColor = createAction(tr("Next &Color")); + connect(m_actionNextColor, SIGNAL(triggered()), SLOT(nextColor())); + + for (auto name : { "1", "2", "A", "C", "E", "F", "G", "H", "I", "J", "L", + "N", "O", "P", "S", "T", "U", "V", "W", "X", "Y", "Z" }) + { + auto action = createAction(); + action->setData(name); + action->setShortcut(QString(name)); + connect(action, SIGNAL(triggered()), SLOT(selectNamedPiece())); + } + + m_actionSetupMode = createAction(tr("S&etup Mode")); + m_actionSetupMode->setCheckable(true); + connect(m_actionSetupMode, SIGNAL(triggered(bool)), SLOT(setupMode(bool))); + + m_actionShowComment = createAction(tr("&Comment")); + m_actionShowComment->setCheckable(true); + m_actionShowComment->setShortcut(QString("Ctrl+T")); + connect(m_actionShowComment, SIGNAL(triggered(bool)), + SLOT(showComment(bool))); + + m_actionRating = createAction(tr("&Rating")); + m_actionRating->setShortcut(QString("F7")); + connect(m_actionRating, SIGNAL(triggered()), SLOT(showRating())); + + m_actionToolBarNoText = createAction(tr("&No Text")); + m_actionToolBarNoText->setActionGroup(groupToolBarText); + m_actionToolBarNoText->setCheckable(true); + connect(m_actionToolBarNoText, SIGNAL(triggered(bool)), + SLOT(toolBarNoText(bool))); + + m_actionToolBarTextBesideIcons = createAction(tr("Text &Beside Icons")); + m_actionToolBarTextBesideIcons->setActionGroup(groupToolBarText); + m_actionToolBarTextBesideIcons->setCheckable(true); + connect(m_actionToolBarTextBesideIcons, SIGNAL(triggered(bool)), + SLOT(toolBarTextBesideIcons(bool))); + + m_actionToolBarTextBelowIcons = createAction(tr("Text Bel&ow Icons")); + m_actionToolBarTextBelowIcons->setActionGroup(groupToolBarText); + m_actionToolBarTextBelowIcons->setCheckable(true); + connect(m_actionToolBarTextBelowIcons, SIGNAL(triggered(bool)), + SLOT(toolBarTextBelowIcons(bool))); + + m_actionToolBarTextOnly = createAction(tr("&Text Only")); + m_actionToolBarTextOnly->setActionGroup(groupToolBarText); + m_actionToolBarTextOnly->setCheckable(true); + connect(m_actionToolBarTextOnly, SIGNAL(triggered(bool)), + SLOT(toolBarTextOnly(bool))); + + m_actionToolBarTextSystem = createAction(tr("&System Default")); + m_actionToolBarTextSystem->setActionGroup(groupToolBarText); + m_actionToolBarTextSystem->setCheckable(true); + connect(m_actionToolBarTextSystem, SIGNAL(triggered(bool)), + SLOT(toolBarTextSystem(bool))); + + m_actionTruncate = createAction(tr("&Truncate")); + connect(m_actionTruncate, SIGNAL(triggered()), SLOT(truncate())); + + m_actionTruncateChildren = createAction(tr("Truncate C&hildren")); + connect(m_actionTruncateChildren, SIGNAL(triggered()), + SLOT(truncateChildren())); + + m_actionShowToolbar = createAction(tr("&Toolbar")); + m_actionShowToolbar->setCheckable(true); + connect(m_actionShowToolbar, SIGNAL(triggered(bool)), + SLOT(showToolbar(bool))); + + m_actionShowVariations = createAction(tr("Show &Variations")); + m_actionShowVariations->setCheckable(true); + connect(m_actionShowVariations, SIGNAL(triggered(bool)), + SLOT(showVariations(bool))); + + m_actionUndo = createAction(tr("&Undo Move")); + setIcon(m_actionUndo, "pentobi-undo"); + connect(m_actionUndo, SIGNAL(triggered()), SLOT(undo())); + + m_actionVariantCallisto2 = + createActionVariant(Variant::callisto_2, + tr("Callisto (&2 Players)")); + m_actionVariantCallisto3 = + createActionVariant(Variant::callisto_3, + tr("Callisto (&3 Players)")); + m_actionVariantCallisto = + createActionVariant(Variant::callisto, + tr("Callisto (&4 Players)")); + m_actionVariantClassic2 = + createActionVariant(Variant::classic_2, + tr("Classic (&2 Players)")); + m_actionVariantClassic3 = + createActionVariant(Variant::classic_3, + tr("Classic (&3 Players)")); + m_actionVariantClassic = + createActionVariant(Variant::classic, tr("Classic (&4 Players)")); + m_actionVariantDuo = createActionVariant(Variant::duo, tr("&Duo")); + m_actionVariantJunior = + createActionVariant(Variant::junior, tr("J&unior")); + m_actionVariantTrigon2 = + createActionVariant(Variant::trigon_2, tr("Trigon (&2 Players)")); + m_actionVariantTrigon3 = + createActionVariant(Variant::trigon_3, tr("Trigon (&3 Players)")); + m_actionVariantTrigon = + createActionVariant(Variant::trigon, tr("Trigon (&4 Players)")); + m_actionVariantNexos2 = + createActionVariant(Variant::nexos_2, tr("Nexos (&2 Players)")); + m_actionVariantNexos = + createActionVariant(Variant::nexos, tr("Nexos (&4 Players)")); + + m_actionVeryBadMove = createAction(tr("V&ery Bad")); + m_actionVeryBadMove->setActionGroup(groupMoveAnnotation); + m_actionVeryBadMove->setCheckable(true); + connect(m_actionVeryBadMove, SIGNAL(triggered(bool)), + SLOT(veryBadMove(bool))); + + m_actionVeryGoodMove = createAction(tr("&Very Good")); + m_actionVeryGoodMove->setActionGroup(groupMoveAnnotation); + m_actionVeryGoodMove->setCheckable(true); + connect(m_actionVeryGoodMove, SIGNAL(triggered(bool)), + SLOT(veryGoodMove(bool))); +} + +QAction* MainWindow::createActionVariant(Variant variant, const QString& text) +{ + auto action = createAction(text); + action->setCheckable(true); + action->setActionGroup(m_actionGroupVariant); + action->setData(static_cast(variant)); + connect(action, SIGNAL(triggered(bool)), SLOT(variantTriggered(bool))); + return action; +} + +QWidget* MainWindow::createCentralWidget() +{ + auto widget = new QWidget; + // We add spacing around and between the two panels using streches (such + // that the spacing grows with the window size) + auto outerLayout = new QVBoxLayout; + widget->setLayout(outerLayout); + auto innerLayout = new QHBoxLayout; + outerLayout->addStretch(1); + outerLayout->addLayout(innerLayout, 100); + outerLayout->addStretch(1); + innerLayout->addStretch(1); + innerLayout->addWidget(createLeftPanel(), 60); + innerLayout->addStretch(1); + innerLayout->addLayout(createRightPanel(), 40); + innerLayout->addStretch(1); + // The central widget doesn't do anything with the focus right now, but we + // allow it to receive the focus such that the user can switch away the + // focus from the comment field and its blinking cursor. + widget->setFocusPolicy(Qt::StrongFocus); + return widget; +} + +QWidget* MainWindow::createLeftPanel() +{ + m_splitter = new QSplitter(Qt::Vertical); + m_guiBoard = new GuiBoard(nullptr, m_bd); + m_splitter->addWidget(m_guiBoard); + m_comment = new QPlainTextEdit; + m_comment->setTabChangesFocus(true); + connect(m_comment, SIGNAL(textChanged()), SLOT(commentChanged())); + m_splitter->addWidget(m_comment); + m_splitter->setStretchFactor(0, 85); + m_splitter->setStretchFactor(1, 15); + m_splitter->setCollapsible(0, false); + m_splitter->setCollapsible(1, false); + return m_splitter; +} + +void MainWindow::createMenu() +{ + auto menuGame = menuBar()->addMenu(tr("&Game")); + menuGame->addAction(m_actionNew); + menuGame->addAction(m_actionRatedGame); + menuGame->addSeparator(); + m_menuVariant = menuGame->addMenu(tr("Game &Variant")); + auto menuClassic = m_menuVariant->addMenu(tr("&Classic")); + menuClassic->addAction(m_actionVariantClassic2); + menuClassic->addAction(m_actionVariantClassic3); + menuClassic->addAction(m_actionVariantClassic); + m_menuVariant->addAction(m_actionVariantDuo); + m_menuVariant->addAction(m_actionVariantJunior); + auto menuTrigon = m_menuVariant->addMenu(tr("&Trigon")); + menuTrigon->addAction(m_actionVariantTrigon2); + menuTrigon->addAction(m_actionVariantTrigon3); + menuTrigon->addAction(m_actionVariantTrigon); + auto menuNexos = m_menuVariant->addMenu(tr("&Nexos")); + menuNexos->addAction(m_actionVariantNexos2); + menuNexos->addAction(m_actionVariantNexos); + auto menuCallisto = m_menuVariant->addMenu(tr("C&allisto")); + menuCallisto->addAction(m_actionVariantCallisto2); + menuCallisto->addAction(m_actionVariantCallisto3); + menuCallisto->addAction(m_actionVariantCallisto); + menuGame->addAction(m_actionGameInfo); + menuGame->addSeparator(); + menuGame->addAction(m_actionUndo); + menuGame->addAction(m_actionFindMove); + menuGame->addSeparator(); + menuGame->addAction(m_actionOpen); + m_menuOpenRecent = menuGame->addMenu(tr("Open R&ecent")); + for (auto& action : m_actionRecentFile) + m_menuOpenRecent->addAction(action); + menuGame->addSeparator(); + menuGame->addAction(m_actionSave); + menuGame->addAction(m_actionSaveAs); + m_menuExport = menuGame->addMenu(tr("E&xport")); + m_menuExport->addAction(m_actionExportImage); + m_menuExport->addAction(m_actionExportAsciiArt); + menuGame->addSeparator(); + menuGame->addAction(m_actionQuit); + + auto menuGo = menuBar()->addMenu(tr("G&o")); + menuGo->addAction(m_actionBeginning); + menuGo->addAction(m_actionBackward); + menuGo->addAction(m_actionForward); + menuGo->addAction(m_actionEnd); + menuGo->addSeparator(); + menuGo->addAction(m_actionNextVariation); + menuGo->addAction(m_actionPreviousVariation); + menuGo->addSeparator(); + menuGo->addAction(m_actionGotoMove); + menuGo->addAction(m_actionBackToMainVariation); + menuGo->addAction(m_actionBeginningOfBranch); + menuGo->addSeparator(); + menuGo->addAction(m_actionFindNextComment); + + auto menuEdit = menuBar()->addMenu(tr("&Edit")); + m_menuMoveAnnotation = menuEdit->addMenu(tr("&Move Annotation")); + m_menuMoveAnnotation->addAction(m_actionNoMoveAnnotation); + m_menuMoveAnnotation->addAction(m_actionVeryGoodMove); + m_menuMoveAnnotation->addAction(m_actionGoodMove); + m_menuMoveAnnotation->addAction(m_actionInterestingMove); + m_menuMoveAnnotation->addAction(m_actionDoubtfulMove); + m_menuMoveAnnotation->addAction(m_actionBadMove); + m_menuMoveAnnotation->addAction(m_actionVeryBadMove); + menuEdit->addSeparator(); + menuEdit->addAction(m_actionMakeMainVariation); + menuEdit->addAction(m_actionMoveUpVariation); + menuEdit->addAction(m_actionMoveDownVariation); + menuEdit->addSeparator(); + menuEdit->addAction(m_actionDeleteAllVariations); + menuEdit->addAction(m_actionTruncate); + menuEdit->addAction(m_actionTruncateChildren); + menuEdit->addAction(m_actionKeepOnlyPosition); + menuEdit->addAction(m_actionKeepOnlySubtree); + menuEdit->addSeparator(); + menuEdit->addAction(m_actionSetupMode); + menuEdit->addAction(m_actionNextColor); + + auto menuView = menuBar()->addMenu(tr("&View")); + menuView->addAction(m_actionShowToolbar); + m_menuToolBarText = menuView->addMenu(tr("Toolbar T&ext")); + m_menuToolBarText->addAction(m_actionToolBarNoText); + m_menuToolBarText->addAction(m_actionToolBarTextBesideIcons); + m_menuToolBarText->addAction(m_actionToolBarTextBelowIcons); + m_menuToolBarText->addAction(m_actionToolBarTextOnly); + m_menuToolBarText->addAction(m_actionToolBarTextSystem); + menuView->addAction(m_actionShowComment); + menuView->addSeparator(); + auto menuMoveNumbers = menuView->addMenu(tr("&Move Marking")); + menuMoveNumbers->addAction(m_actionMoveMarkingLastDot); + menuMoveNumbers->addAction(m_actionMoveMarkingLastNumber); + menuMoveNumbers->addAction(m_actionMoveMarkingAllNumber); + menuMoveNumbers->addAction(m_actionMoveMarkingNone); + menuView->addAction(m_actionCoordinates); + menuView->addAction(m_actionShowVariations); + menuView->addSeparator(); + menuView->addAction(m_actionFullscreen); + + auto menuComputer = menuBar()->addMenu(tr("&Computer")); + menuComputer->addAction(m_actionComputerColors); + menuComputer->addAction(m_actionPlay); + menuComputer->addSeparator(); + menuComputer->addAction(m_actionPlaySingleMove); + menuComputer->addAction(m_actionInterrupt); + menuComputer->addSeparator(); + m_menuLevel = menuComputer->addMenu(QString()); + m_menuLevel->addActions(m_actionGroupLevel->actions()); + + auto menuTools = menuBar()->addMenu(tr("&Tools")); + menuTools->addAction(m_actionRating); + menuTools->addAction(m_actionAnalyzeGame); + + auto menuHelp = menuBar()->addMenu(tr("&Help")); + menuHelp->addAction(m_actionHelp); + menuHelp->addAction(m_actionAbout); +} + +QLayout* MainWindow::createOrientationButtonBoxLeft() +{ + auto outerLayout = new QVBoxLayout; + auto layout = new QGridLayout; + layout->addWidget(createOBoxToolButton(m_actionRotateAnticlockwise), 0, 0); + layout->addWidget(createOBoxToolButton(m_actionRotateClockwise), 0, 1); + layout->addWidget(createOBoxToolButton(m_actionFlipHorizontally), 1, 0); + layout->addWidget(createOBoxToolButton(m_actionFlipVertically), 1, 1); + outerLayout->addStretch(); + outerLayout->addLayout(layout); + outerLayout->addStretch(); + return outerLayout; +} + +QLayout* MainWindow::createOrientationButtonBoxRight() +{ + auto outerLayout = new QVBoxLayout; + auto layout = new QGridLayout; + layout->addWidget(createOBoxToolButton(m_actionPreviousPiece), 0, 0); + layout->addWidget(createOBoxToolButton(m_actionNextPiece), 0, 1); + layout->addWidget(createOBoxToolButton(m_actionClearPiece), 1, 0, + 1, 2, Qt::AlignHCenter); + outerLayout->addStretch(); + outerLayout->addLayout(layout); + outerLayout->addStretch(); + return outerLayout; +} + +QLayout* MainWindow::createOrientationSelector() +{ + auto layout = new QHBoxLayout; + layout->addStretch(); + layout->addLayout(createOrientationButtonBoxLeft()); + layout->addSpacing(8); + m_orientationDisplay = new OrientationDisplay(nullptr, m_bd); + connect(m_orientationDisplay, SIGNAL(colorClicked(Color)), + SLOT(orientationDisplayColorClicked(Color))); + m_orientationDisplay->setSizePolicy(QSizePolicy::MinimumExpanding, + QSizePolicy::MinimumExpanding); + layout->addWidget(m_orientationDisplay); + layout->addSpacing(8); + layout->addLayout(createOrientationButtonBoxRight()); + layout->addStretch(); + return layout; +} + +QLayout* MainWindow::createRightPanel() +{ + auto layout = new QVBoxLayout; + layout->addLayout(createOrientationSelector(), 20); + m_scoreDisplay = new ScoreDisplay; + layout->addWidget(m_scoreDisplay, 5); + auto pieceSelectorLayout = new SameHeightLayout; + layout->addLayout(pieceSelectorLayout, 80); + for (Color c : Color::Range(Color::range)) + { + m_pieceSelector[c] = new PieceSelector(nullptr, m_bd, c); + connect(m_pieceSelector[c], + SIGNAL(pieceSelected(Color,Piece,const Transform*)), + SLOT(selectPiece(Color,Piece,const Transform*))); + pieceSelectorLayout->addWidget(m_pieceSelector[c]); + } + return layout; +} + +void MainWindow::deleteAllVariations() +{ + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Delete all variations?"), + tr("All variations but the main variation will be" + " removed from the game tree.")); + auto deleteButton = + msgBox.addButton(tr("Delete Variations"), QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != deleteButton) + return; + bool currentNodeChanges = ! is_main_variation(m_game.get_current()); + if (currentNodeChanges) + cancelThread(); + m_game.delete_all_variations(); + updateWindow(currentNodeChanges); +} + +void MainWindow::doubtfulMove(bool checked) +{ + if (! checked) + return; + m_game.set_doubtful_move(); + updateWindow(false); +} + +void MainWindow::createToolBar() +{ + auto toolBar = new QToolBar; + toolBar->setMovable(false); + toolBar->setContextMenuPolicy(Qt::PreventContextMenu); + toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); + toolBar->addAction(m_actionNew); + toolBar->addAction(m_actionRatedGame); + toolBar->addAction(m_actionUndo); + toolBar->addSeparator(); + toolBar->addAction(m_actionComputerColors); + toolBar->addAction(m_actionPlay); + toolBar->addSeparator(); + toolBar->addAction(m_actionBeginning); + toolBar->addAction(m_actionBackward); + toolBar->addAction(m_actionForward); + toolBar->addAction(m_actionEnd); + toolBar->addSeparator(); + toolBar->addAction(m_actionNextVariation); + toolBar->addAction(m_actionPreviousVariation); + // Is this the right way to enable autorepeat buttons? Using + // QAction::autoRepeat applies only to keyboard and adding a QToolButton + // with QToolBar::addWidget() makes the tool button not respect the + // toolButtonStyle. + for (auto button : toolBar->findChildren()) + { + auto action = button->defaultAction(); + if (action == m_actionBackward || action == m_actionForward) + button->setAutoRepeat(true); + } + addToolBar(toolBar); + initToolBarText(toolBar); + QSettings settings; + auto toolBarText = settings.value("toolbar_text", "system").toString(); + if (toolBarText == "no_text") + m_actionToolBarNoText->setChecked(true); + else if (toolBarText == "beside_icons") + m_actionToolBarTextBesideIcons->setChecked(true); + else if (toolBarText == "below_icons") + m_actionToolBarTextBelowIcons->setChecked(true); + else if (toolBarText == "text_only") + m_actionToolBarTextOnly->setChecked(true); + else + m_actionToolBarTextSystem->setChecked(true); +} + +void MainWindow::deleteAutoSaveFile() +{ + QString autoSaveFile = getAutoSaveFile(); + QFile file(autoSaveFile); + if (file.exists() && ! file.remove()) + showError(tr("Could not delete %1").arg(autoSaveFile)); +} + +void MainWindow::enablePieceSelector(Color c) +{ + for (Color i : m_bd.get_colors()) + { + m_pieceSelector[i]->checkUpdate(); + m_pieceSelector[i]->setEnabled(i == c); + } +} + +void MainWindow::end() +{ + gotoNode(get_last_node(m_game.get_current())); +} + +bool MainWindow::eventFilter(QObject* object, QEvent* event) +{ + // Empty status tips can clear the status bar if the mouse goes over a + // menu. We don't want that because it deletes our "computer is thinking" + // message. This still happens with Qt 5.6 on some platforms. + if (event->type() == QEvent::StatusTip) + return true; + return QMainWindow::eventFilter(object, event); +} + +void MainWindow::exportAsciiArt() +{ + QString file = QFileDialog::getSaveFileName(this, "", getLastDir(), + tr("Text files (*.txt);;All files (*)")); + if (file.isEmpty()) + return; + rememberDir(file); + ofstream out(file.toLocal8Bit().constData()); + m_bd.write(out, false); + if (! out) + showError(QString::fromLocal8Bit(strerror(errno))); +} + +void MainWindow::exportImage() +{ + QSettings settings; + auto size = settings.value("export_image_size", 420).toInt(); + QInputDialog dialog(this); + dialog.setWindowFlags(dialog.windowFlags() + & ~Qt::WindowContextHelpButtonHint); + dialog.setWindowTitle(tr("Export Image")); + dialog.setLabelText(tr("Image size:")); + dialog.setInputMode(QInputDialog::IntInput); + dialog.setIntRange(0, 2147483647); + dialog.setIntStep(40); + dialog.setIntValue(size); + if (! dialog.exec()) + return; + size = dialog.intValue(); + settings.setValue("export_image_size", size); + bool coordinates = m_actionCoordinates->isChecked(); + BoardPainter boardPainter; + boardPainter.setCoordinates(coordinates); + boardPainter.setCoordinateColor(QColor(100, 100, 100)); + QImage image(size, size, QImage::Format_ARGB32); + image.fill(Qt::transparent); + QPainter painter; + painter.begin(&image); + if (coordinates) + painter.fillRect(0, 0, size, size, QColor(216, 216, 216)); + boardPainter.paintEmptyBoard(painter, size, size, m_bd.get_variant(), + m_bd.get_geometry()); + Grid pieceId; + if (m_bd.get_board_type() == BoardType::nexos) + { + pieceId.fill(0, m_bd.get_geometry()); + unsigned n = 0; + for (Color c : m_bd.get_colors()) + for (Move mv : m_bd.get_setup().placements[c]) + { + ++n; + for (Point p : m_bd.get_move_points(mv)) + pieceId[p] = n; + } + for (auto mv : m_bd.get_moves()) + { + ++n; + for (Point p : m_bd.get_move_points(mv.move)) + pieceId[p] = n; + } + } + boardPainter.paintPieces(painter, m_bd.get_point_state(), pieceId, + &m_guiBoard->getLabels()); + painter.end(); + QString file; + while (true) + { + file = QFileDialog::getSaveFileName(this, file, getLastDir()); + if (file.isEmpty()) + break; + rememberDir(file); + QImageWriter writer(file); + if (writer.write(image)) + break; + else + showError(writer.errorString()); + } +} + +void MainWindow::findMove() +{ + if (m_bd.is_game_over()) + return; + if (! m_legalMoves) + m_legalMoves.reset(new MoveList); + Color to_play = m_bd.get_to_play(); + if (m_legalMoves->empty()) + { + if (! m_marker) + m_marker.reset(new MoveMarker); + m_bd.gen_moves(to_play, *m_marker, *m_legalMoves); + m_marker->clear(*m_legalMoves); + sort(m_legalMoves->begin(), m_legalMoves->end(), + [&](Move mv1, Move mv2) { + return getHeuristic(m_bd, mv1) > getHeuristic(m_bd, mv2); + }); + } + if (m_legalMoves->empty()) + return; + if (m_legalMoveIndex >= m_legalMoves->size()) + m_legalMoveIndex = 0; + auto mv = (*m_legalMoves)[m_legalMoveIndex]; + selectPiece(to_play, m_bd.get_move_piece(mv)); + m_guiBoard->showMove(to_play, mv); + ++m_legalMoveIndex; +} + +void MainWindow::findNextComment() +{ + auto& root = m_game.get_root(); + auto& current = m_game.get_current(); + auto node = find_next_comment(current); + if (! node && ¤t != &root) + { + QMessageBox msgBox(this); + initQuestion(msgBox, tr("The end of the tree was reached."), + tr("Continue the search from the start of the tree?")); + auto continueButton = + msgBox.addButton(tr("Continue From Start"), + QMessageBox::AcceptRole); + msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(continueButton); + msgBox.exec(); + if (msgBox.clickedButton() == continueButton) + { + node = &root; + if (! has_comment(*node)) + node = find_next_comment(*node); + } + else + return; + } + if (! node) + { + showInfo(tr("No comment found")); + return; + } + showComment(true); + gotoNode(*node); +} + +void MainWindow::flipHorizontally() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = m_bd.get_transforms().get_mirrored_horizontally(transform); + transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); +} + +void MainWindow::flipVertically() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = m_bd.get_transforms().get_mirrored_vertically(transform); + transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); +} + +void MainWindow::forward() +{ + gotoNode(m_game.get_current().get_first_child_or_null()); +} + +void MainWindow::fullscreen() +{ + if (isFullScreen()) + { + // If F11 is pressed in fullscreen, we switch to normal + leaveFullscreen(); + return; + } + QSettings settings; + menuBar()->hide(); + findChild()->hide(); + settings.setValue("geometry", saveGeometry()); + m_wasMaximized = isMaximized(); + showFullScreen(); + if (! m_leaveFullscreenButton) + m_leaveFullscreenButton = + new LeaveFullscreenButton(this, m_actionLeaveFullscreen); + m_leaveFullscreenButton->showButton(); +} + +void MainWindow::gameInfo() +{ + GameInfoDialog dialog(this, m_game); + dialog.exec(); + updateWindow(false); +} + +void MainWindow::gameOver() +{ + auto variant = m_bd.get_variant(); + auto nuColors = get_nu_colors(variant); + auto nuPlayers = get_nu_players(variant); + bool breakTies = (m_bd.get_piece_set() == PieceSet::callisto); + QString info; + if (nuColors == 2) + { + auto score = m_bd.get_score_twocolor(Color(0)); + if (score == 1) + info = tr("Blue wins with 1 point."); + else if (score > 0) + info = tr("Blue wins with %1 points.").arg(score); + else if (score == -1) + info = tr("Green wins with 1 point."); + else if (score < 0) + info = tr("Green wins with %1 points.").arg(-score); + else if (breakTies) + info = tr("Green wins (tie resolved)."); + else + info = tr("The game ends in a tie."); + } + else if (nuPlayers == 2) + { + LIBBOARDGAME_ASSERT(nuColors == 4); + auto score = m_bd.get_score_multicolor(Color(0)); + if (score == 1) + info = tr("Blue/Red wins with 1 point."); + else if (score > 0) + info = tr("Blue/Red wins with %1 points.").arg(score); + else if (score == -1) + info = tr("Yellow/Green wins with 1 point."); + else if (score < 0) + info = tr("Yellow/Green wins with %1 points.").arg(-score); + else if (breakTies) + info = tr("Yellow/Green wins (tie resolved)."); + else + info = tr("The game ends in a tie."); + } + else if (nuPlayers == 3) + { + auto blue = m_bd.get_points(Color(0)); + auto yellow = m_bd.get_points(Color(1)); + auto red = m_bd.get_points(Color(2)); + auto maxPoints = max(blue, max(yellow, red)); + if (breakTies && red == maxPoints + && (blue == maxPoints || yellow == maxPoints)) + info = tr("Red wins (tie resolved)."); + else if (breakTies && yellow == maxPoints && blue == maxPoints) + info = tr("Yellow wins (tie resolved)."); + else if (blue == yellow && yellow == red) + info = tr("The game ends in a tie between all colors."); + else if (blue == maxPoints && blue == yellow) + info = tr("The game ends in a tie between Blue and Yellow."); + else if (blue == maxPoints && blue == red) + info = tr("The game ends in a tie between Blue and Red."); + else if (yellow == maxPoints && yellow == red) + info = tr("The game ends in a tie between Yellow and Red."); + else if (blue == maxPoints) + info = tr("Blue wins."); + else if (yellow == maxPoints) + info = tr("Yellow wins."); + else + info = tr("Red wins."); + } + else + { + LIBBOARDGAME_ASSERT(nuPlayers == 4); + auto blue = m_bd.get_points(Color(0)); + auto yellow = m_bd.get_points(Color(1)); + auto red = m_bd.get_points(Color(2)); + auto green = m_bd.get_points(Color(3)); + auto maxPoints = max(blue, max(yellow, max(red, green))); + if (breakTies && green == maxPoints + && (red == maxPoints || blue == maxPoints + || yellow == maxPoints)) + info = tr("Green wins (tie resolved)."); + else if (breakTies && red == maxPoints + && (blue == maxPoints || yellow == maxPoints)) + info = tr("Red wins (tie resolved)."); + else if (breakTies && yellow == maxPoints && blue == maxPoints) + info = tr("Yellow wins (tie resolved)."); + else if (blue == yellow && yellow == red && red == green) + info = tr("The game ends in a tie between all colors."); + else if (blue == maxPoints && blue == yellow && yellow == red) + info = tr("The game ends in a tie between Blue, Yellow and Red."); + else if (blue == maxPoints && blue == yellow && yellow == green) + info = + tr("The game ends in a tie between Blue, Yellow and Green."); + else if (blue == maxPoints && blue == red && red == green) + info = tr("The game ends in a tie between Blue, Red and Green."); + else if (yellow == maxPoints && yellow == red && red == green) + info = tr("The game ends in a tie between Yellow, Red and Green."); + else if (blue == maxPoints && blue == yellow) + info = tr("The game ends in a tie between Blue and Yellow."); + else if (blue == maxPoints && blue == red) + info = tr("The game ends in a tie between Blue and Red."); + else if (blue == maxPoints && blue == green) + info = tr("The game ends in a tie between Blue and Green."); + else if (yellow == maxPoints && yellow == red) + info = tr("The game ends in a tie between Yellow and Red."); + else if (yellow == maxPoints && yellow == green) + info = tr("The game ends in a tie between Yellow and Green."); + else if (red == maxPoints && red == green) + info = tr("The game ends in a tie between Red and Green."); + else if (blue == maxPoints) + info = tr("Blue wins."); + else if (yellow == maxPoints) + info = tr("Yellow wins."); + else if (red == maxPoints) + info = tr("Red wins."); + else + info = tr("Green wins."); + } + if (m_isRated) + { + QString detailText; + int oldRating = m_history->getRating().to_int(); + unsigned place; + bool isPlaceShared; + m_bd.get_place(m_ratedGameColor, place, isPlaceShared); + float gameResult; + if (place == 0 && !isPlaceShared) + gameResult = 1; + else if (place == 0 && isPlaceShared) + gameResult = 0.5; + else + gameResult = 0; + unsigned nuOpp = get_nu_players(variant) - 1; + Rating oppRating = m_player->get_rating(variant); + QString date = QString(PentobiTree::get_date_today().c_str()); + m_history->addGame(gameResult, oppRating, nuOpp, m_ratedGameColor, + gameResult, date, m_level, m_game.get_tree()); + if (m_ratingDialog) + m_ratingDialog->updateContent(); + int newRating = m_history->getRating().to_int(); + if (newRating > oldRating) + detailText = tr("Your rating has increased from %1 to %2.") + .arg(QString::number(oldRating), QString::number(newRating)); + else if (newRating == oldRating) + detailText = tr("Your rating stays at %1.").arg(oldRating); + else + detailText = + tr("Your rating has decreased from %1 to %2.") + .arg(QString::number(oldRating), QString::number(newRating)); + setRated(false); + QSettings settings; + auto key = QString("next_rated_random_") + to_string_id(variant); + settings.remove(key); + QMessageBox msgBox(this); + Util::setNoTitle(msgBox); + msgBox.setIcon(QMessageBox::Information); + msgBox.setText(info); + msgBox.setInformativeText(detailText); + auto showRatingButton = + msgBox.addButton(tr("Show &Rating"), QMessageBox::AcceptRole); + msgBox.addButton(QMessageBox::Close); + msgBox.setDefaultButton(showRatingButton); + msgBox.exec(); + auto result = msgBox.clickedButton(); + if (result == showRatingButton) + showRating(); + } + else + showInfo(info); +} + +void MainWindow::genMove(bool playSingleMove) +{ + cancelThread(); + ++m_genMoveId; + setCursor(QCursor(Qt::BusyCursor)); + m_actionNextPiece->setEnabled(false); + m_actionPreviousPiece->setEnabled(false); + m_actionPlay->setEnabled(false); + m_actionPlaySingleMove->setEnabled(false); + m_actionInterrupt->setEnabled(true); + showStatus(tr("Computer is thinking...")); + clearPiece(); + clear_abort(); + m_lastRemainingSeconds = 0; + m_lastRemainingMinutes = 0; + m_player->set_level(m_level); + QFuture future = + QtConcurrent::run(this, &MainWindow::asyncGenMove, m_bd.get_to_play(), + m_genMoveId, playSingleMove); + m_genMoveWatcher.setFuture(future); + m_isGenMoveRunning = true; +} + +void MainWindow::genMoveFinished() +{ + m_actionInterrupt->setEnabled(false); + clearStatus(); + GenMoveResult result = m_genMoveWatcher.future().result(); + if (result.genMoveId != m_genMoveId) + { + // Callback from a move generation canceled with cancelThread() + return; + } + LIBBOARDGAME_ASSERT(m_isGenMoveRunning); + m_isGenMoveRunning = false; + setCursor(QCursor(Qt::ArrowCursor)); + if (get_abort() && computerPlaysAll()) + m_computerColors.fill(false); + Color c = result.color; + auto mv = result.move; + if (mv.is_null()) + { + // No need to translate, should never happen if program is correct + showError("Computer failed to generate a move"); + return; + } + if (! m_bd.is_legal(c, mv)) + { + // No need to translate, should never happen if program is correct + showError("Computer generated illegal move"); + return; + } + play(c, mv); + // Call updateWindow() before checkComputerMove() because checkComputerMove + // resets m_lastComputerMovesBegin if computer doesn't play current color + // and updateWindow needs m_lastComputerMovesBegin + updateWindow(true); + if (! result.playSingleMove) + checkComputerMove(); +} + +QString MainWindow::getFilter() const +{ + return tr("Blokus games (*.blksgf);;All files (*)"); +} + +QString MainWindow::getLastDir() +{ + QSettings settings; + auto dir = settings.value("last_dir", "").toString(); + if (dir.isEmpty() || ! QFileInfo::exists(dir)) + dir = QStandardPaths::writableLocation( + QStandardPaths::DocumentsLocation); + return dir; +} + +QString MainWindow::getVersion() const +{ + QString version; +#ifdef VERSION + version = VERSION; +#endif + if (version.isEmpty()) + version = "UNKNOWN"; + return version; +} + +void MainWindow::goodMove(bool checked) +{ + if (! checked) + return; + m_game.set_good_move(); + updateWindow(false); +} + +void MainWindow::gotoMove() +{ + vector nodes; + auto& tree = m_game.get_tree(); + auto node = &m_game.get_current(); + do + { + if (! tree.get_move(*node).is_null()) + nodes.insert(nodes.begin(), node); + node = node->get_parent_or_null(); + } + while (node); + node = m_game.get_current().get_first_child_or_null(); + while (node) + { + if (! tree.get_move(*node).is_null()) + nodes.push_back(node); + node = node->get_first_child_or_null(); + } + int maxMoves = int(nodes.size()); + if (maxMoves == 0) + return; + int defaultValue = m_bd.get_nu_moves(); + if (defaultValue == 0) + defaultValue = maxMoves; + QInputDialog dialog(this); + dialog.setWindowFlags(dialog.windowFlags() + & ~Qt::WindowContextHelpButtonHint); + dialog.setWindowTitle(tr("Go to Move")); + dialog.setLabelText(tr("Move number:")); + dialog.setInputMode(QInputDialog::IntInput); + dialog.setIntRange(1, static_cast(nodes.size())); + dialog.setIntStep(1); + dialog.setIntValue(defaultValue); + if (dialog.exec()) + gotoNode(*nodes[dialog.intValue() - 1]); +} + +void MainWindow::gotoNode(const SgfNode& node) +{ + cancelThread(); + leaveSetupMode(); + try + { + m_game.goto_node(node); + } + catch (const InvalidTree& e) + { + showInvalidFile(m_file, e); + return; + } + if (m_analyzeGameWindow && m_analyzeGameWindow->isVisible()) + m_analyzeGameWindow->analyzeGameWidget + ->setCurrentPosition(m_game, node); + m_autoPlay = false; + updateWindow(true); +} + +void MainWindow::gotoNode(const SgfNode* node) +{ + if (node) + gotoNode(*node); +} + +void MainWindow::gotoPosition(Variant variant, + const vector& moves) +{ + if (m_bd.get_variant() != variant) + return; + auto& tree = m_game.get_tree(); + auto node = &tree.get_root(); + if (tree.has_move_ignore_invalid(*node)) + // Move in root node not supported. + return; + for (ColorMove mv : moves) + { + bool found = false; + for (auto& i : node->get_children()) + if (tree.get_move(i) == mv) + { + found = true; + node = &i; + break; + } + if (! found) + return; + } + gotoNode(*node); +} + +void MainWindow::help() +{ + if (m_helpWindow) + { + m_helpWindow->show(); + m_helpWindow->raise(); + return; + } + QString path = HelpWindow::findMainPage(m_helpDir, "pentobi"); + m_helpWindow = new HelpWindow(nullptr, tr("Pentobi Help"), path); + initToolBarText(m_helpWindow->findChild()); + m_helpWindow->show(); +} + +void MainWindow::initGame() +{ + setRated(false); + if (m_analyzeGameWindow) + { + delete m_analyzeGameWindow; + m_analyzeGameWindow = nullptr; + } + m_game.init(); + m_game.set_charset("UTF-8"); +#ifdef VERSION + m_game.set_application("Pentobi", VERSION); +#else + m_game.set_application("Pentobi"); +#endif + m_game.set_date_today(); + m_game.clear_modified(); + QSettings settings; + if (! settings.value("computer_color_none").toBool()) + { + for (Color c : Color::Range(m_bd.get_nu_nonalt_colors())) + m_computerColors[c] = ! m_bd.is_same_player(c, Color(0)); + m_autoPlay = true; + } + else + { + m_computerColors.fill(false); + m_autoPlay = false; + } + leaveSetupMode(); + m_gameFinished = false; + m_isAutoSaveLoaded = false; + setFile(""); +} + +void MainWindow::initVariantActions() +{ + // Use a temporary const variable to avoid that QList detaches in for loop + const auto actions = m_actionGroupVariant->actions(); + for (auto action : actions) + if (Variant(action->data().toInt()) == m_bd.get_variant()) + { + action->setChecked(true); + return; + } +} + +void MainWindow::initPieceSelectors() +{ + for (Color::IntType i = 0; i < Color::range; ++i) + { + bool isVisible = (i < m_bd.get_nu_colors()); + m_pieceSelector[Color(i)]->setVisible(isVisible); + if (isVisible) + m_pieceSelector[Color(i)]->init(); + } +} + +void MainWindow::interestingMove(bool checked) +{ + if (! checked) + return; + m_game.set_interesting_move(); + updateWindow(false); +} + +void MainWindow::interrupt() +{ + cancelThread(); + m_autoPlay = false; +} + +void MainWindow::interruptPlay() +{ + if (! m_isGenMoveRunning) + return; + set_abort(); + m_autoPlay = false; +} + +bool MainWindow::isComputerToPlay() const +{ + Color to_play = m_bd.get_to_play(); + if (m_game.get_variant() != Variant::classic_3 || to_play != Color(3)) + return m_computerColors[to_play]; + return m_computerColors[Color(m_bd.get_alt_player())]; +} + +void MainWindow::keepOnlyPosition() +{ + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Keep only position?"), + tr("All previous and following moves and variations will" + " be removed from the game tree.")); + auto keepOnlyPositionButton = + msgBox.addButton(tr("Keep Only Position"), + QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != keepOnlyPositionButton) + return; + cancelThread(); + m_game.keep_only_position(); + updateWindow(true); +} + +void MainWindow::keepOnlySubtree() +{ + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Keep only subtree?"), + tr("All previous moves and variations will be removed" + " from the game tree.")); + auto keepOnlySubtreeButton = + msgBox.addButton(tr("Keep Only Subtree"), + QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != keepOnlySubtreeButton) + return; + cancelThread(); + m_game.keep_only_subtree(); + updateWindow(true); +} + +void MainWindow::leaveFullscreen() +{ + if (! isFullScreen()) + return; + QSettings settings; + auto showToolbar = settings.value("toolbar", true).toBool(); + menuBar()->show(); + findChild()->setVisible(showToolbar); + // m_leaveFullscreenButton can be null if the window was put in fullscreen + // mode by a "generic" method by the window manager (e.g. the title bar + // menu on KDE) and not by MainWindow::fullscreen() + if (m_leaveFullscreenButton) + m_leaveFullscreenButton->hideButton(); + // Call showNormal() even if m_wasMaximized otherwise restoring the + // maximized window state does not work correctly on Xfce + showNormal(); + if (m_wasMaximized) + showMaximized(); +} + +void MainWindow::leaveSetupMode() +{ + if (! m_actionSetupMode->isChecked()) + return; + setupMode(false); +} + +void MainWindow::levelTriggered(bool checked) +{ + if (checked) + setLevel(qobject_cast(sender())->data().toUInt()); +} + +void MainWindow::loadHistory() +{ + auto variant = m_game.get_variant(); + if (m_history->getVariant() == variant) + return; + m_history->load(variant); + if (m_ratingDialog) + m_ratingDialog->updateContent(); +} + +void MainWindow::makeMainVariation() +{ + m_game.make_main_variation(); + updateWindow(false); +} + +void MainWindow::moveDownVariation() +{ + m_game.move_down_variation(); + updateWindow(false); +} + +void MainWindow::moveUpVariation() +{ + m_game.move_up_variation(); + updateWindow(false); +} + +void MainWindow::nextColor() +{ + m_game.set_to_play(m_bd.get_next(m_bd.get_to_play())); + auto to_play = m_bd.get_to_play(); + m_orientationDisplay->selectColor(to_play); + clearPiece(); + for (Color c : m_bd.get_colors()) + m_pieceSelector[c]->setEnabled(to_play == c); + if (m_actionSetupMode->isChecked()) + setSetupPlayer(); + updateWindow(false); +} + +void MainWindow::nextPiece() +{ + auto c = m_bd.get_to_play(); + if (m_bd.get_pieces_left(c).empty()) + return; + auto nuUniqPieces = m_bd.get_nu_uniq_pieces(); + Piece::IntType i; + Piece selectedPiece = m_guiBoard->getSelectedPiece(); + if (! selectedPiece.is_null()) + i = static_cast(selectedPiece.to_int() + 1); + else + i = 0; + while (true) + { + if (i >= nuUniqPieces) + i = 0; + if (m_bd.is_piece_left(c, Piece(i))) + break; + ++i; + } + selectPiece(c, Piece(i)); +} + +void MainWindow::nextTransform() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = m_bd.get_piece_info(piece).get_next_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); +} + +void MainWindow::nextVariation() +{ + gotoNode(m_game.get_current().get_sibling()); +} + +void MainWindow::newGame() +{ + if (! checkSave()) + return; + cancelThread(); + initGame(); + deleteAutoSaveFile(); + updateWindow(true); +} + +void MainWindow::noMoveAnnotation(bool checked) +{ + if (! checked) + return; + m_game.remove_move_annotation(); + updateWindow(false); +} + +void MainWindow::open() +{ + if (! checkSave()) + return; + QString file = QFileDialog::getOpenFileName(this, tr("Open"), getLastDir(), + getFilter()); + if (file.isEmpty()) + return; + rememberDir(file); + if (open(file)) + rememberFile(file); +} + +bool MainWindow::open(const QString& file, bool isTemporary) +{ + if (file.isEmpty()) + return false; + cancelThread(); + TreeReader reader; + ifstream in(file.toLocal8Bit().constData()); + try + { + reader.read(in); + } + catch (const TreeReader::ReadError& e) + { + if (! in) + { + QString text = + tr("Could not read file '%1'").arg(QFileInfo(file).fileName()); + showError(text, QString::fromLocal8Bit(strerror(errno))); + } + else + { + showInvalidFile(file, e); + } + return false; + } + m_isAutoSaveLoaded = false; + if (! isTemporary) + { + setFile(file); + deleteAutoSaveFile(); + } + if (m_analyzeGameWindow) + { + delete m_analyzeGameWindow; + m_analyzeGameWindow = nullptr; + } + setRated(false); + try + { + auto tree = reader.get_tree_transfer_ownership(); + m_game.init(tree); + if (! libpentobi_base::node_util::has_setup(m_game.get_root())) + m_game.goto_node(get_last_node(m_game.get_root())); + initPieceSelectors(); + } + catch (const InvalidTree& e) + { + showInvalidFile(file, e); + } + m_computerColors.fill(false); + m_autoPlay = false; + leaveSetupMode(); + initVariantActions(); + restoreLevel(m_bd.get_variant()); + updateWindow(true); + loadHistory(); + return true; +} + +void MainWindow::openRecentFile() +{ + auto action = qobject_cast(sender()); + if (action) + openCheckSave(action->data().toString()); +} + +void MainWindow::openCheckSave(const QString& file) +{ + if (checkSave()) + open(file); +} + +void MainWindow::orientationDisplayColorClicked(Color) +{ + if (m_actionSetupMode->isChecked()) + nextColor(); +} + +void MainWindow::placePiece(Color c, Move mv) +{ + cancelThread(); + bool isSetupMode = m_actionSetupMode->isChecked(); + bool isAltColor = + (m_bd.get_variant() == Variant::classic_3 && c.to_int() == 3); + if ((! isAltColor && m_computerColors[c]) + || (isAltColor && m_computerColors[Color(m_bd.get_alt_player())]) + || isSetupMode) + // If the user enters a move previously played by the computer (e.g. + // after undoing moves) then it is unlikely that the user wants to keep + // the computer color settings. + m_computerColors.fill(false); + if (isSetupMode) + { + m_game.add_setup(c, mv); + setSetupPlayer(); + updateWindow(true); + } + else + { + play(c, mv); + updateWindow(true); + checkComputerMove(); + } +} + +void MainWindow::play() +{ + cancelThread(); + leaveSetupMode(); + if (! isComputerToPlay()) + { + m_computerColors.fill(false); + auto c = m_bd.get_to_play(); + if (m_bd.get_variant() == Variant::classic_3 && c == Color(3)) + m_computerColors[Color(m_bd.get_alt_player())] = true; + else + { + m_computerColors[c] = true; + m_computerColors[m_bd.get_second_color(c)] = true; + } + QSettings settings; + settings.setValue("computer_color_none", false); + } + m_autoPlay = true; + genMove(); +} + +void MainWindow::play(Color c, Move mv) +{ + m_game.play(c, mv, false); + m_gameFinished = false; + if (m_bd.is_game_over()) + { + updateWindow(true); + repaint(); + gameOver(); + m_gameFinished = true; + deleteAutoSaveFile(); + return; + } +} + +void MainWindow::playSingleMove() +{ + cancelThread(); + leaveSetupMode(); + m_autoPlay = false; + genMove(true); +} + +void MainWindow::pointClicked(Point p) +{ + // If a piece on the board is clicked on in setup mode, remove it and make + // it the selected piece without changing its orientation. + if (! m_actionSetupMode->isChecked()) + return; + PointState s = m_bd.get_point_state(p); + if (s.is_empty()) + return; + Color c = s.to_color(); + Move mv = m_bd.get_move_at(p); + m_game.remove_setup(c, mv); + setSetupPlayer(); + updateWindow(true); + selectPiece(c, m_bd.get_move_piece(mv), m_bd.find_transform(mv)); + m_guiBoard->setSelectedPiecePoints(mv); +} + +void MainWindow::previousPiece() +{ + auto c = m_bd.get_to_play(); + if (m_bd.get_pieces_left(c).empty()) + return; + auto nuUniqPieces = m_bd.get_nu_uniq_pieces(); + Piece::IntType i; + Piece selectedPiece = m_guiBoard->getSelectedPiece(); + if (! selectedPiece.is_null()) + i = selectedPiece.to_int(); + else + i = 0; + while (true) + { + if (i == 0) + i = static_cast(nuUniqPieces - 1); + else + --i; + if (m_bd.is_piece_left(c, Piece(i))) + break; + } + selectPiece(c, Piece(i)); +} + +void MainWindow::previousTransform() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = + m_bd.get_piece_info(piece).get_previous_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); +} + +void MainWindow::previousVariation() +{ + gotoNode(m_game.get_current().get_previous_sibling()); +} + +void MainWindow::ratedGame() +{ + if (! checkSave()) + return; + cancelThread(); + if (m_history->getNuGames() == 0) + { + InitialRatingDialog dialog(this); + if (dialog.exec() != QDialog::Accepted) + return; + m_history->init(Rating(static_cast(dialog.getRating()))); + } + int level; + QSettings settings; + unsigned random; + auto variant = m_bd.get_variant(); + auto key = QString("next_rated_random_") + to_string_id(variant); + if (settings.contains(key)) + random = settings.value(key).toUInt(); + else + { + // RandomGenerator::ResultType may be larger than unsigned + random = static_cast(m_random.generate() % 1000); + settings.setValue(key, random); + } + m_history->getNextRatedGameSettings(m_maxLevel, random, + level, m_ratedGameColor); + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Start rated game?"), + "" + + tr("In this game, you play %1 against Pentobi level %2.") + .arg(getPlayerString(variant, m_ratedGameColor), + QString::number(level))); + auto startGameButton = + msgBox.addButton(tr("&Start Game"), QMessageBox::AcceptRole); + msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(startGameButton); + msgBox.exec(); + auto result = msgBox.clickedButton(); + if (result != startGameButton) + return; + setLevel(level); + initGame(); + setFile(""); + setRated(true); + m_computerColors.fill(true); + for (Color c : Color::Range(m_bd.get_nu_nonalt_colors())) + if (m_bd.is_same_player(c, m_ratedGameColor)) + m_computerColors[c] = false; + m_autoPlay = true; + QString computerPlayerName = + //: The first argument is the version of Pentobi + tr("Pentobi %1 (level %2)").arg(getVersion(), QString::number(level)); + string charset = m_game.get_root().get_property("CA", ""); + string computerPlayerNameStdStr = + Util::convertSgfValueFromQString(computerPlayerName, charset); + string humanPlayerNameStdStr = + Util::convertSgfValueFromQString(tr("Human"), charset); + for (Color c : Color::Range(m_bd.get_nu_nonalt_colors())) + if (m_computerColors[c]) + m_game.set_player_name(c, computerPlayerNameStdStr); + else + m_game.set_player_name(c, humanPlayerNameStdStr); + // Setting the player names marks the game as modified but there is nothing + // important that would need to be saved yet + m_game.clear_modified(); + deleteAutoSaveFile(); + updateWindow(true); + checkComputerMove(); +} + +void MainWindow::rememberDir(const QString& file) +{ + if (file.isEmpty()) + return; + QString canonicalFile = file; + QString canonicalFilePath = QFileInfo(file).canonicalFilePath(); + if (! canonicalFilePath.isEmpty()) + canonicalFile = canonicalFilePath; + QFileInfo info(canonicalFile); + QSettings settings; + settings.setValue("last_dir", info.dir().path()); +} + +void MainWindow::rememberFile(const QString& file) +{ + if (file.isEmpty()) + return; + QString canonicalFile = file; + QString canonicalFilePath = QFileInfo(file).canonicalFilePath(); + if (! canonicalFilePath.isEmpty()) + canonicalFile = canonicalFilePath; + QSettings settings; + auto files = settings.value("recent_files").toStringList(); + files.removeAll(canonicalFile); + files.prepend(canonicalFile); + while (files.size() > maxRecentFiles) + files.removeLast(); + settings.setValue("recent_files", files); + settings.sync(); // updateRecentFiles() needs the new settings + updateRecentFiles(); +} + +void MainWindow::restoreLevel(Variant variant) +{ + QSettings settings; + QString key = QString("level_") + to_string_id(variant); + m_level = settings.value(key, 1).toInt(); + if (m_level < 1) + m_level = 1; + if (m_level > m_maxLevel) + m_level = m_maxLevel; + m_actionGroupLevel->actions().at(m_level - 1)->setChecked(true); +} + +void MainWindow::rotateAnticlockwise() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = m_bd.get_transforms().get_rotated_anticlockwise(transform); + transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); + updateFlipActions(); +} + +void MainWindow::rotateClockwise() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = m_bd.get_transforms().get_rotated_clockwise(transform); + transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); + updateFlipActions(); +} + +void MainWindow::save() +{ + if (m_file.isEmpty()) + saveAs(); + else if (save(m_file)) + { + m_game.clear_modified(); + updateWindow(false); + } +} + +bool MainWindow::save(const QString& file) +{ + if (! writeGame(file.toLocal8Bit().constData())) + { + showError(tr("The file could not be saved."), + /*: Error message if file cannot be saved. %1 is + replaced by the file name, %2 by the error message + of the operating system. */ + tr("%1: %2").arg(file, + QString::fromLocal8Bit(strerror(errno)))); + return false; + } + else + { + Util::removeThumbnail(file); + return true; + } +} + +void MainWindow::saveAs() +{ + QString file = m_file; + if (file.isEmpty()) + { + file = getLastDir(); + file.append(QDir::separator()); + file.append(tr("Untitled Game.blksgf")); + if (QFileInfo::exists(file)) + for (unsigned i = 1; ; ++i) + { + file = getLastDir(); + file.append(QDir::separator()); + file.append(tr("Untitled Game %1.blksgf").arg(i)); + if (! QFileInfo::exists(file)) + break; + } + } + file = QFileDialog::getSaveFileName(this, tr("Save"), file, getFilter()); + if (! file.isEmpty()) + { + rememberDir(file); + if (save(file)) + { + m_game.clear_modified(); + updateWindow(false); + } + setFile(file); + rememberFile(file); + } +} + +void MainWindow::searchCallback(double elapsedSeconds, double remainingSeconds) +{ + // If the search is longer than 10 sec, we show the (maximum) remaining + // time (only during a move generation, ignore search callbacks during + // game analysis) + if (! m_isGenMoveRunning || elapsedSeconds < 10) + return; + QString text; + int seconds = static_cast(ceil(remainingSeconds)); + if (seconds < 90) + { + if (seconds == m_lastRemainingSeconds) + return; + m_lastRemainingSeconds = seconds; + text = + tr("Computer is thinking... (up to %1 seconds remaining)") + .arg(seconds); + } + else + { + int minutes = static_cast(ceil(remainingSeconds / 60)); + if (minutes == m_lastRemainingMinutes) + return; + m_lastRemainingMinutes = minutes; + text = + tr("Computer is thinking... (up to %1 minutes remaining)") + .arg(minutes); + } + QMetaObject::invokeMethod(statusBar(), "showMessage", Q_ARG(QString, text), + Q_ARG(int, 0)); +} + +void MainWindow::selectNamedPiece() +{ + string name(qobject_cast(sender())->data().toString() + .toLocal8Bit().constData()); + auto c = m_bd.get_to_play(); + Board::PiecesLeftList pieces; + for (Piece::IntType i = 0; i < m_bd.get_nu_uniq_pieces(); ++i) + { + Piece piece(i); + if (m_bd.is_piece_left(c, piece) + && m_bd.get_piece_info(piece).get_name().find(name) == 0) + pieces.push_back(piece); + } + if (pieces.empty()) + return; + auto piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + piece = pieces[0]; + else + { + auto pos = std::find(pieces.begin(), pieces.end(), piece); + if (pos == pieces.end()) + piece = pieces[0]; + else + { + ++pos; + if (pos == pieces.end()) + piece = pieces[0]; + else + piece = *pos; + } + } + selectPiece(c, piece); +} + +void MainWindow::selectPiece(Color c, Piece piece) +{ + selectPiece(c, piece, m_bd.get_transforms().get_default()); +} + +void MainWindow::selectPiece(Color c, Piece piece, const Transform* transform) +{ + if (m_isGenMoveRunning + || (m_bd.is_game_over() && ! m_actionSetupMode->isChecked())) + return; + m_game.set_to_play(c); + m_guiBoard->selectPiece(c, piece); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->selectColor(c); + m_orientationDisplay->setSelectedPiece(piece); + m_orientationDisplay->setSelectedPieceTransform(transform); + bool can_rotate = m_bd.get_piece_info(piece).can_rotate(); + m_actionRotateClockwise->setEnabled(can_rotate); + m_actionRotateAnticlockwise->setEnabled(can_rotate); + updateFlipActions(); + m_actionClearPiece->setEnabled(true); +} + +void MainWindow::setCommentText(const QString& text) +{ + m_ignoreCommentTextChanged = true; + m_comment->setPlainText(text); + m_ignoreCommentTextChanged = false; + if (! text.isEmpty()) + m_comment->ensureCursorVisible(); + m_comment->clearFocus(); + updateWindow(false); +} + +void MainWindow::setNoDelay() +{ + m_noDelay = true; +} + +void MainWindow::setVariant(Variant variant) +{ + if (m_bd.get_variant() == variant) + return; + if (! checkSave()) + { + initVariantActions(); + return; + } + cancelThread(); + QSettings settings; + settings.setValue("variant", to_string_id(variant)); + clearPiece(); + m_game.init(variant); + initPieceSelectors(); + newGame(); + loadHistory(); + restoreLevel(variant); +} + +void MainWindow::setFile(const QString& file) +{ + m_file = file; + // Don't use setWindowFilePath() because of QTBUG-16507 + if (m_file.isEmpty()) + setWindowTitle(tr("Pentobi")); + else + setWindowTitle(tr("[*]%1").arg(QFileInfo(m_file).fileName())); +} + +void MainWindow::setLevel(unsigned level) +{ + if (level < 1 || level > m_maxLevel) + return; + m_level = level; + m_actionGroupLevel->actions().at(level - 1)->setChecked(true); + QSettings settings; + settings.setValue(QString("level_") + to_string_id(m_bd.get_variant()), + m_level); +} + +void MainWindow::setMoveMarkingAllNumber(bool checked) +{ + if (! checked) + return; + QSettings settings; + settings.setValue("move_marking", "all_number"); + updateWindow(false); +} + +void MainWindow::setMoveMarkingLastDot(bool checked) +{ + if (! checked) + return; + QSettings settings; + settings.setValue("move_marking", "last_dot"); + updateWindow(false); +} + +void MainWindow::setMoveMarkingLastNumber(bool checked) +{ + if (! checked) + return; + QSettings settings; + settings.setValue("move_marking", "last_number"); + updateWindow(false); +} + +void MainWindow::setMoveMarkingNone(bool checked) +{ + if (! checked) + return; + QSettings settings; + settings.setValue("move_marking", "none"); + updateWindow(false); +} + +void MainWindow::setPlayToolTip() +{ + m_actionPlay->setToolTip( + m_computerColors[m_bd.get_to_play()] ? + tr("Make the computer continue to play the current color") : + tr("Make the computer play the current color")); +} + +void MainWindow::setRated(bool isRated) +{ + m_isRated = isRated; + if (isRated) + { + statusBar()->addWidget(m_ratedGameLabelText); + m_ratedGameLabelText->show(); + } + else if (m_ratedGameLabelText->isVisible()) + statusBar()->removeWidget(m_ratedGameLabelText); +} + +void MainWindow::setSetupPlayer() +{ + if (! m_game.has_setup()) + m_game.remove_player(); + else + m_game.set_player(m_bd.get_to_play()); +} + +void MainWindow::setTitleMenuLevel() +{ + QString title; + switch (m_game.get_variant()) + { + case Variant::classic: + title = tr("&Level (Classic, 4 Players)"); + break; + case Variant::classic_2: + title = tr("&Level (Classic, 2 Players)"); + break; + case Variant::classic_3: + title = tr("&Level (Classic, 3 Players)"); + break; + case Variant::duo: + title = tr("&Level (Duo)"); + break; + case Variant::trigon: + title = tr("&Level (Trigon, 4 Players)"); + break; + case Variant::trigon_2: + title = tr("&Level (Trigon, 2 Players)"); + break; + case Variant::trigon_3: + title = tr("&Level (Trigon, 3 Players)"); + break; + case Variant::junior: + title = tr("&Level (Junior)"); + break; + case Variant::nexos: + title = tr("&Level (Nexos, 4 Players)"); + break; + case Variant::nexos_2: + title = tr("&Level (Nexos, 2 Players)"); + break; + case Variant::callisto: + title = tr("&Level (Callisto, 4 Players)"); + break; + case Variant::callisto_2: + title = tr("&Level (Callisto, 2 Players)"); + break; + case Variant::callisto_3: + title = tr("&Level (Callisto, 3 Players)"); + break; + } + m_menuLevel->setTitle(title); +} + +void MainWindow::setupMode(bool enable) +{ + // Currently, we allow setup mode only if no moves have been played. It + // should also work in inner nodes but this might be confusing for users + // and violate some assumptions in the user interface (e.g. node depth is + // equal to move number). Therefore, m_actionSetupMode is disabled if the + // root node has children, but we still need to check for it here because + // due to bugs in the Unitiy interface in Ubuntu 11.10, menu items are + // not always disabled if the corresponding action is disabled. + if (enable && m_game.get_root().has_children()) + { + showInfo(tr("Setup mode cannot be used if moves have been played.")); + enable = false; + } + m_actionSetupMode->setChecked(enable); + m_guiBoard->setFreePlacement(enable); + if (enable) + { + m_setupModeLabel->show(); + for (Color c : m_bd.get_colors()) + m_pieceSelector[c]->setEnabled(true); + m_computerColors.fill(false); + } + else + { + setSetupPlayer(); + m_setupModeLabel->hide(); + enablePieceSelector(m_bd.get_to_play()); + updateWindow(false); + } +} + +void MainWindow::showComment(bool checked) +{ + QSettings settings; + bool wasVisible = m_comment->isVisible(); + if (wasVisible && ! checked) + settings.setValue("splitter_state", m_splitter->saveState()); + settings.setValue("show_comment", checked); + m_comment->setVisible(checked); + if (! wasVisible && checked) + m_splitter->restoreState( + settings.value("splitter_state").toByteArray()); + +} + +void MainWindow::showError(const QString& text, const QString& infoText, + const QString& detailText) +{ + ::showError(this, text, infoText, detailText); +} + +void MainWindow::showInfo(const QString& text, const QString& infoText, + const QString& detailText, bool withIcon) +{ + ::showInfo(this, text, infoText, detailText, withIcon); +} + +void MainWindow::showInvalidFile(QString file, const exception& e) +{ + showError(tr("Error in file '%1'").arg(QFileInfo(file).fileName()), + tr("The file is not a valid Blokus SGF file."), e.what()); +} + +void MainWindow::showRating() +{ + if (! m_ratingDialog) + { + m_ratingDialog = new RatingDialog(this, *m_history); + connect(m_ratingDialog, SIGNAL(openRecentFile(const QString&)), + SLOT(openCheckSave(const QString&))); + } + loadHistory(); + m_ratingDialog->show(); +} + +void MainWindow::showStatus(const QString& text, bool temporary) +{ + int timeout = (temporary ? 4000 : 0); + statusBar()->showMessage(text, timeout); +} + +void MainWindow::showToolbar(bool checked) +{ + QSettings settings; + settings.setValue("toolbar", checked); + findChild()->setVisible(checked); + m_menuToolBarText->setEnabled(checked); +} + +QSize MainWindow::sizeHint() const +{ + auto geo = QApplication::desktop()->screenGeometry(); + return QSize(geo.width() * 2 / 3, min(geo.width() * 4 / 10, geo.height())); +} + +void MainWindow::toolBarNoText(bool checked) +{ + if (checked) + toolBarText("no_text", Qt::ToolButtonIconOnly); +} + +void MainWindow::toolBarText(const QString& key, Qt::ToolButtonStyle style) +{ + QSettings settings; + settings.setValue("toolbar_text", key); + findChild()->setToolButtonStyle(style); + if (m_helpWindow) + m_helpWindow->findChild()->setToolButtonStyle(style); +} + +void MainWindow::toolBarTextBesideIcons(bool checked) +{ + if (checked) + toolBarText("beside_icons", Qt::ToolButtonTextBesideIcon); +} + +void MainWindow::toolBarTextBelowIcons(bool checked) +{ + if (checked) + toolBarText("below_icons", Qt::ToolButtonTextUnderIcon); +} + +void MainWindow::toolBarTextOnly(bool checked) +{ + if (checked) + toolBarText("text_only", Qt::ToolButtonTextOnly); +} + +void MainWindow::toolBarTextSystem(bool checked) +{ + if (checked) + toolBarText("system", Qt::ToolButtonFollowStyle); +} + +void MainWindow::truncate() +{ + auto& current = m_game.get_current(); + if (! current.has_parent()) + return; + cancelThread(); + if (current.has_children()) + { + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Truncate this subtree?"), + tr("This position and all following moves and" + " variations will be removed from the game tree.")); + auto truncateButton = + msgBox.addButton(tr("Truncate"), + QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != truncateButton) + return; + } + m_game.truncate(); + m_autoPlay = false; + m_gameFinished = false; + updateWindow(true); +} + +void MainWindow::truncateChildren() +{ + if (! m_game.get_current().has_children()) + return; + cancelThread(); + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Truncate children?"), + tr("All following moves and variations will" + " be removed from the game tree.")); + auto truncateButton = + msgBox.addButton(tr("Truncate Children"), + QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != truncateButton) + return; + m_game.truncate_children(); + m_gameFinished = false; + updateWindow(false); +} + +void MainWindow::showVariations(bool checked) +{ + { + QSettings settings; + settings.setValue("show_variations", checked); + } + updateWindow(false); +} + +void MainWindow::undo() +{ + auto& current = m_game.get_current(); + if (current.has_children() + || ! m_game.get_tree().has_move_ignore_invalid(current) + || ! current.has_parent()) + return; + cancelThread(); + m_game.undo(); + m_autoPlay = false; + m_gameFinished = false; + updateWindow(true); +} + +void MainWindow::updateComment() +{ + string comment = m_game.get_comment(); + if (comment.empty()) + { + setCommentText(""); + return; + } + string charset = m_game.get_root().get_property("CA", ""); + setCommentText(Util::convertSgfValueToQString(comment, charset)); +} + +void MainWindow::updateFlipActions() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + bool can_flip_horizontally = + m_bd.get_piece_info(piece).can_flip_horizontally(transform); + m_actionFlipHorizontally->setEnabled(can_flip_horizontally); + bool can_flip_vertically = + m_bd.get_piece_info(piece).can_flip_vertically(transform); + m_actionFlipVertically->setEnabled(can_flip_vertically); +} + +void MainWindow::updateMoveAnnotationActions() +{ + if (m_game.get_move_ignore_invalid().is_null()) + { + m_menuMoveAnnotation->setEnabled(false); + return; + } + m_menuMoveAnnotation->setEnabled(true); + double goodMove = m_game.get_good_move(); + if (goodMove > 1) + { + m_actionVeryGoodMove->setChecked(true); + return; + } + if (goodMove > 0) + { + m_actionGoodMove->setChecked(true); + return; + } + double badMove = m_game.get_bad_move(); + if (badMove > 1) + { + m_actionVeryBadMove->setChecked(true); + return; + } + if (badMove > 0) + { + m_actionBadMove->setChecked(true); + return; + } + if (m_game.is_interesting_move()) + { + m_actionInterestingMove->setChecked(true); + return; + } + if (m_game.is_doubtful_move()) + { + m_actionDoubtfulMove->setChecked(true); + return; + } + m_actionNoMoveAnnotation->setChecked(true); +} + +void MainWindow::updateMoveNumber() +{ + auto& tree = m_game.get_tree(); + auto& current = m_game.get_current(); + unsigned move = get_move_number(tree, current); + unsigned movesLeft = get_moves_left(tree, current); + unsigned totalMoves = move + movesLeft; + string variation = get_variation_string(current); + QString text = + QString::fromLocal8Bit(get_position_info(tree, current).c_str()); + QString toolTip; + if (variation.empty()) + { + if (movesLeft == 0) + { + if (move > 0) + toolTip = tr("Move %1").arg(move); + } + else + { + if (move == 0) + toolTip = tr("%n move(s)", "", totalMoves); + else + toolTip = tr("Move %1 of %2").arg(QString::number(move), + QString::number(totalMoves)); + } + } + else + toolTip = tr("Move %1 of %2 in variation %3") + .arg(QString::number(move), QString::number(totalMoves), + variation.c_str()); + if (text.isEmpty()) + { + if (m_moveNumber->isVisible()) + statusBar()->removeWidget(m_moveNumber); + } + else + { + m_moveNumber->setText(text); + m_moveNumber->setToolTip(toolTip); + if (! m_moveNumber->isVisible()) + { + statusBar()->addPermanentWidget(m_moveNumber); + m_moveNumber->show(); + } + } +} + +void MainWindow::updateRecentFiles() +{ + QSettings settings; + auto files = settings.value("recent_files").toStringList(); + for (int i = 0; i < files.size(); ++i) + if (! QFileInfo::exists(files[i])) + { + files.removeAt(i); + --i; + } + int nuRecentFiles = files.size(); + if (nuRecentFiles > maxRecentFiles) + nuRecentFiles = maxRecentFiles; + m_menuOpenRecent->setEnabled(nuRecentFiles > 0); + for (int i = 0; i < nuRecentFiles; ++i) + { + QFileInfo info = QFileInfo(files[i]); + QString name = info.absoluteFilePath(); + // Don't prepend the filename by a number for a shortcut key + // because the file name may contain underscores and Ubuntu Unity does + // not handle this correctly (Unity bug #1390373) + m_actionRecentFile[i]->setText(name); + m_actionRecentFile[i]->setData(files[i]); + m_actionRecentFile[i]->setVisible(true); + } + for (int j = nuRecentFiles; j < maxRecentFiles; ++j) + m_actionRecentFile[j]->setVisible(false); +} + +void MainWindow::updateWindow(bool currentNodeChanged) +{ + updateWindowModified(); + m_guiBoard->copyFromBoard(m_bd); + QSettings settings; + auto markVariations = settings.value("show_variations", true).toBool(); + unsigned nuMoves = m_bd.get_nu_moves(); + unsigned markMovesBegin, markMovesEnd; + if (m_actionMoveMarkingAllNumber->isChecked()) + { + markMovesBegin = 1; + markMovesEnd = nuMoves; + } + else if (m_actionMoveMarkingLastNumber->isChecked() + || m_actionMoveMarkingLastDot->isChecked()) + { + markMovesBegin = nuMoves; + markMovesEnd = nuMoves; + } + else + { + markMovesBegin = 0; + markMovesEnd = 0; + } + gui_board_util::setMarkup(*m_guiBoard, m_game, markMovesBegin, + markMovesEnd, markVariations, + m_actionMoveMarkingLastDot->isChecked()); + m_scoreDisplay->updateScore(m_bd); + if (m_legalMoves) + m_legalMoves->clear(); + m_legalMoveIndex = 0; + bool isGameOver = m_bd.is_game_over(); + auto to_play = m_bd.get_to_play(); + if (isGameOver && ! m_actionSetupMode->isChecked()) + m_orientationDisplay->clearSelectedColor(); + else + m_orientationDisplay->selectColor(to_play); + if (currentNodeChanged) + { + clearPiece(); + for (Color c : m_bd.get_colors()) + m_pieceSelector[c]->checkUpdate(); + if (! m_actionSetupMode->isChecked()) + enablePieceSelector(to_play); + updateComment(); + updateMoveAnnotationActions(); + } + updateMoveNumber(); + setPlayToolTip(); + auto& tree = m_game.get_tree(); + auto& current = m_game.get_current(); + bool isMain = is_main_variation(current); + bool hasEarlierVariation = has_earlier_variation(current); + bool hasParent = current.has_parent(); + bool hasChildren = current.has_children(); + bool hasMove = tree.has_move_ignore_invalid(current); + bool hasMoves = m_bd.has_moves(to_play); + bool isEmpty = libboardgame_sgf::util::is_empty(tree); + bool hasNextVar = current.get_sibling(); + bool hasPrevVar = current.get_previous_sibling(); + m_actionAnalyzeGame->setEnabled(! m_isRated + && tree.has_main_variation_moves()); + m_actionBackToMainVariation->setEnabled(! isMain); + m_actionBeginning->setEnabled(! m_isRated && hasParent); + m_actionBeginningOfBranch->setEnabled(hasEarlierVariation); + m_actionBackward->setEnabled(! m_isRated && hasParent); + m_actionComputerColors->setEnabled(! m_isRated); + m_actionDeleteAllVariations->setEnabled(tree.has_variations()); + m_actionFindNextComment->setEnabled(! m_isRated); + m_actionForward->setEnabled(hasChildren); + m_actionEnd->setEnabled(hasChildren); + m_actionFindMove->setEnabled(! isGameOver); + m_actionGotoMove->setEnabled(! m_isRated && + hasCurrentVariationOtherMoves(tree, current)); + m_actionKeepOnlyPosition->setEnabled(! m_isRated + && (hasParent || hasChildren)); + m_actionKeepOnlySubtree->setEnabled(hasParent && hasChildren); + m_actionGroupLevel->setEnabled(! m_isRated); + m_actionMakeMainVariation->setEnabled(! isMain); + m_actionMoveDownVariation->setEnabled(hasNextVar); + m_actionMoveUpVariation->setEnabled(hasPrevVar); + m_actionNew->setEnabled(! isEmpty); + m_actionNextVariation->setEnabled(hasNextVar); + if (! m_isGenMoveRunning) + { + m_actionNextPiece->setEnabled(! isGameOver); + m_actionPreviousPiece->setEnabled(! isGameOver); + m_actionPlay->setEnabled(! m_isRated && hasMoves); + m_actionPlaySingleMove->setEnabled(! m_isRated && hasMoves); + } + m_actionPreviousVariation->setEnabled(hasPrevVar); + m_actionRatedGame->setEnabled(! m_isRated); + m_actionSave->setEnabled(! m_file.isEmpty() && m_game.is_modified()); + m_actionSaveAs->setEnabled(! isEmpty || m_game.is_modified()); + // See also comment in setupMode() + m_actionSetupMode->setEnabled(! m_isRated && ! hasParent && ! hasChildren); + m_actionNextColor->setEnabled(! m_isRated); + m_actionTruncate->setEnabled(! m_isRated && hasParent); + m_actionTruncateChildren->setEnabled(hasChildren); + m_actionUndo->setEnabled(! m_isRated && hasParent && ! hasChildren + && hasMove); + m_actionGroupVariant->setEnabled(! m_isRated); + m_menuVariant->setEnabled(! m_isRated); + setTitleMenuLevel(); +} + +void MainWindow::updateWindowModified() +{ + if (! m_file.isEmpty()) + setWindowModified(m_game.is_modified()); +} + +void MainWindow::variantTriggered(bool checked) +{ + if (checked) + setVariant(Variant(qobject_cast(sender())->data().toInt())); +} + +void MainWindow::veryBadMove(bool checked) +{ + if (! checked) + return; + m_game.set_bad_move(2); + updateWindow(false); +} + +void MainWindow::veryGoodMove(bool checked) +{ + if (! checked) + return; + m_game.set_good_move(2); + updateWindow(false); +} + +void MainWindow::wheelEvent(QWheelEvent* event) +{ + int delta = event->delta() / 8 / 15; + if (delta > 0) + { + if (! m_guiBoard->getSelectedPiece().is_null()) + for (int i = 0; i < delta; ++i) + nextTransform(); + } + else if (delta < 0) + { + if (! m_guiBoard->getSelectedPiece().is_null()) + for (int i = 0; i < -delta; ++i) + previousTransform(); + } + event->accept(); +} + +bool MainWindow::writeGame(const string& file) +{ + ofstream out(file); + PentobiTreeWriter writer(out, m_game.get_tree()); + writer.set_indent(1); + writer.write(); + return static_cast(out); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/MainWindow.h b/src/pentobi/MainWindow.h new file mode 100644 index 0000000..fd9332c --- /dev/null +++ b/src/pentobi/MainWindow.h @@ -0,0 +1,707 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/MainWindow.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_MAIN_WINDOW_H +#define PENTOBI_MAIN_WINDOW_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include "RatingHistory.h" +#include "libboardgame_util/RandomGenerator.h" +#include "libpentobi_base/ColorMap.h" +#include "libpentobi_base/Game.h" +#include "libpentobi_mcts/Player.h" + +class QActionGroup; +class QLabel; +class QPlainTextEdit; +class QSplitter; +class AnalyzeGameWindow; +class GuiBoard; +class HelpWindow; +class LeaveFullscreenButton; +class OrientationDisplay; +class PieceSelector; +class RatingDialog; +class ScoreDisplay; + +using namespace std; +using libboardgame_sgf::SgfNode; +using libboardgame_base::Transform; +using libboardgame_util::ArrayList; +using libboardgame_util::RandomGenerator; +using libpentobi_base::Board; +using libpentobi_base::ColorMap; +using libpentobi_base::ColorMove; +using libpentobi_base::Game; +using libpentobi_base::Move; +using libpentobi_base::MoveList; +using libpentobi_base::MoveMarker; +using libpentobi_base::Piece; +using libpentobi_base::Point; +using libpentobi_base::Variant; +using libpentobi_mcts::Player; + +//----------------------------------------------------------------------------- + +class MainWindow + : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(Variant variant, const QString& initialFile = "", + const QString& helpDir = "", + unsigned maxLevel = Player::max_supported_level, + const QString& booksDir = "", bool noBook = false, + unsigned nuThreads = 0); + + ~MainWindow(); + + bool eventFilter(QObject* object, QEvent* event) override; + + QSize sizeHint() const override; + +public slots: + void about(); + + void analyzeGame(); + + void backward(); + + void backToMainVariation(); + + void beginning(); + + void beginningOfBranch(); + + void clearPiece(); + + void computerColors(); + + void deleteAllVariations(); + + void end(); + + void exportAsciiArt(); + + void exportImage(); + + void findMove(); + + void findNextComment(); + + void flipHorizontally(); + + void flipVertically(); + + void forward(); + + void gotoMove(); + + /** Go to a node if a node with a position defined by a sequence of moves + still exists. */ + void gotoPosition(Variant variant, const vector& moves); + + void help(); + + void gameInfo(); + + /** Abort current move generation and don't play a move. */ + void interrupt(); + + /** Abort current move generation and play best move found so far. */ + void interruptPlay(); + + void keepOnlyPosition(); + + void keepOnlySubtree(); + + void makeMainVariation(); + + void moveDownVariation(); + + void moveUpVariation(); + + void newGame(); + + void nextColor(); + + void nextVariation(); + + void nextPiece(); + + void nextTransform(); + + void open(); + + bool open(const QString& file, bool isTemporary = false); + + void placePiece(Color c, Move mv); + + void play(); + + void playSingleMove(); + + void pointClicked(Point p); + + void previousPiece(); + + void previousTransform(); + + void previousVariation(); + + void ratedGame(); + + void rotateAnticlockwise(); + + void rotateClockwise(); + + void save(); + + void saveAs(); + + void selectPiece(Color c, Piece piece); + + void selectPiece(Color c, Piece piece, const Transform* transform); + + void setLevel(unsigned level); + + void truncate(); + + void truncateChildren(); + + void undo(); + + void showToolbar(bool checked); + + void showVariations(bool checked); + + void showRating(); + + void setNoDelay(); + +protected: + void closeEvent(QCloseEvent* event) override; + + void wheelEvent(QWheelEvent* event) override; + +private: + struct GenMoveResult + { + bool playSingleMove; + + Color color; + + Move move; + + unsigned genMoveId; + }; + + static const int maxRecentFiles = 9; + + Game m_game; + + const Board& m_bd; + + unique_ptr m_player; + + bool m_noDelay = false; + + /** Was window maximized before entering fullscreen. */ + bool m_wasMaximized = false; + + bool m_isGenMoveRunning = false; + + bool m_isAnalyzeRunning = false; + + /** Should the computer generate a move if it is its turn? + Enabled on game start (if the computer plays at least one color) + or after selecting Play. Disabled when navigating in the game. */ + bool m_autoPlay = false; + + /** Flag indicating that the position after the last move played was + a terminal position. */ + bool m_gameFinished; + + bool m_isRated = false; + + /** Flag set while setting the text in m_comment for fast return in the + textChanged() handler. + Used because QPlainTextEdit does not have a textEdited() signal and + we only need to handle edits. */ + bool m_ignoreCommentTextChanged = false; + + /** Color played by the user in a rated game. + Only defined if m_isRated is true. In game variants with multiple + colors per player, the user plays all colors of the player with + this color. */ + Color m_ratedGameColor; + + /** Integer ID assigned to the currently running move generation. + Used to ignore finished events from canceled move generations. */ + unsigned m_genMoveId = 0; + + unsigned m_maxLevel; + + /** Current playing level of m_player. + Only use if m_useTimeLimit is false. Possible values for m_level are in + 1..maxLevel. Only used if m_timeLimit is zero. Stored independently of + the player and set at the player before each move generation, such that + setting a new level does not require to abort a running move + generation. */ + unsigned m_level; + + RandomGenerator m_random; + + unique_ptr m_history; + + /** Local variable in findMove(). + Reused for efficiency. */ + unique_ptr m_marker; + + GuiBoard* m_guiBoard; + + QString m_helpDir; + + ColorMap m_computerColors; + + ColorMap m_pieceSelector; + + OrientationDisplay* m_orientationDisplay; + + ScoreDisplay* m_scoreDisplay; + + QSplitter* m_splitter; + + QPlainTextEdit* m_comment; + + HelpWindow* m_helpWindow = nullptr; + + RatingDialog* m_ratingDialog = nullptr; + + AnalyzeGameWindow* m_analyzeGameWindow = nullptr; + + QAction* m_actionAbout; + + QAction* m_actionAnalyzeGame; + + QAction* m_actionBackward; + + QAction* m_actionBackToMainVariation; + + QAction* m_actionBadMove; + + QAction* m_actionBeginning; + + QAction* m_actionBeginningOfBranch; + + QAction* m_actionClearPiece; + + QAction* m_actionComputerColors; + + QAction* m_actionCoordinates; + + QAction* m_actionDeleteAllVariations; + + QAction* m_actionDoubtfulMove; + + QAction* m_actionEnd; + + QAction* m_actionExportAsciiArt; + + QAction* m_actionExportImage; + + QAction* m_actionFindMove; + + QAction* m_actionFindNextComment; + + QAction* m_actionFlipHorizontally; + + QAction* m_actionFlipVertically; + + QAction* m_actionForward; + + QAction* m_actionFullscreen; + + QAction* m_actionGameInfo; + + QAction* m_actionGoodMove; + + QAction* m_actionGotoMove; + + QAction* m_actionHelp; + + QAction* m_actionInterestingMove; + + QAction* m_actionInterrupt; + + QAction* m_actionInterruptPlay; + + QAction* m_actionKeepOnlyPosition; + + QAction* m_actionKeepOnlySubtree; + + QAction* m_actionLeaveFullscreen; + + QAction* m_actionMakeMainVariation; + + QAction* m_actionMoveDownVariation; + + QAction* m_actionMoveMarkingAllNumber; + + QAction* m_actionMoveMarkingLastDot; + + QAction* m_actionMoveMarkingLastNumber; + + QAction* m_actionMoveMarkingNone; + + QAction* m_actionMoveUpVariation; + + QAction* m_actionMovePieceLeft; + + QAction* m_actionMovePieceRight; + + QAction* m_actionMovePieceUp; + + QAction* m_actionMovePieceDown; + + QAction* m_actionNextColor; + + QAction* m_actionNextPiece; + + QAction* m_actionNextTransform; + + QAction* m_actionNextVariation; + + QAction* m_actionNew; + + QAction* m_actionRatedGame; + + QAction* m_actionNoMoveAnnotation; + + QAction* m_actionOpen; + + QAction* m_actionPlacePiece; + + QAction* m_actionPlay; + + QAction* m_actionPlaySingleMove; + + QAction* m_actionPreviousPiece; + + QAction* m_actionPreviousTransform; + + QAction* m_actionPreviousVariation; + + QAction* m_actionQuit; + + QAction* m_actionRecentFile[maxRecentFiles]; + + QAction* m_actionRotateAnticlockwise; + + QAction* m_actionRotateClockwise; + + QAction* m_actionSave; + + QAction* m_actionSaveAs; + + QAction* m_actionShowComment; + + QAction* m_actionRating; + + QAction* m_actionShowToolbar; + + QAction* m_actionSetupMode; + + QAction* m_actionToolBarNoText; + + QAction* m_actionToolBarTextBesideIcons; + + QAction* m_actionToolBarTextBelowIcons; + + QAction* m_actionToolBarTextOnly; + + QAction* m_actionToolBarTextSystem; + + QAction* m_actionTruncate; + + QAction* m_actionTruncateChildren; + + QAction* m_actionShowVariations; + + QAction* m_actionUndo; + + QAction* m_actionVariantCallisto; + + QAction* m_actionVariantCallisto2; + + QAction* m_actionVariantCallisto3; + + QAction* m_actionVariantClassic; + + QAction* m_actionVariantClassic2; + + QAction* m_actionVariantClassic3; + + QAction* m_actionVariantDuo; + + QAction* m_actionVariantJunior; + + QAction* m_actionVariantNexos; + + QAction* m_actionVariantNexos2; + + QAction* m_actionVariantTrigon; + + QAction* m_actionVariantTrigon2; + + QAction* m_actionVariantTrigon3; + + QAction* m_actionVeryGoodMove; + + QAction* m_actionVeryBadMove; + + QActionGroup* m_actionGroupLevel; + + QActionGroup* m_actionGroupVariant; + + QMenu* m_menuExport; + + QMenu* m_menuLevel; + + QMenu* m_menuMoveAnnotation; + + QMenu* m_menuOpenRecent; + + QMenu* m_menuToolBarText; + + QMenu* m_menuVariant; + + QLabel* m_setupModeLabel; + + QLabel* m_ratedGameLabelText; + + QFutureWatcher m_genMoveWatcher; + + QString m_file; + + unique_ptr m_legalMoves; + + unsigned m_legalMoveIndex; + + QLabel* m_moveNumber; + + LeaveFullscreenButton* m_leaveFullscreenButton = nullptr; + + int m_lastRemainingSeconds; + + int m_lastRemainingMinutes; + + /** Is the current game a game loaded from the autosave file? + If yes, we need it to save again on quit even if it was not modified. + Note that the autosave game is deleted after loading to avoid that + it is used twice if two instances of Pentobi are started. */ + bool m_isAutoSaveLoaded; + + + GenMoveResult asyncGenMove(Color c, int genMoveId, bool playSingleMove); + + bool checkSave(); + + bool checkQuit(); + + void clearFile(); + + QAction* createAction(const QString& text = ""); + + QAction* createActionLevel(unsigned level, const QString& text); + + void createActions(); + + QAction* createActionVariant(Variant variant, const QString& text); + + QWidget* createCentralWidget(); + + QWidget* createLeftPanel(); + + void createMenu(); + + QLayout* createOrientationButtonBoxLeft(); + + QLayout* createOrientationButtonBoxRight(); + + QLayout* createOrientationSelector(); + + QLayout* createRightPanel(); + + void createToolBar(); + + void cancelThread(); + + void checkComputerMove(); + + void clearStatus(); + + bool computerPlaysAll() const; + + void deleteAutoSaveFile(); + + void enablePieceSelector(Color c); + + void gameOver(); + + void genMove(bool playSingleMove = false); + + QString getFilter() const; + + QString getLastDir(); + + QString getVersion() const; + + void gotoNode(const SgfNode& node); + + void gotoNode(const SgfNode* node); + + void initGame(); + + void initVariantActions(); + + void initPieceSelectors(); + + bool isComputerToPlay() const; + + void leaveSetupMode(); + + void play(Color c, Move mv); + + void restoreLevel(Variant variant); + + bool save(const QString& file); + + void searchCallback(double elapsedSeconds, double remainingSeconds); + + void setCommentText(const QString& text); + + void setVariant(Variant variant); + + void setPlayToolTip(); + + void setRated(bool isRated); + + void setFile(const QString& file); + + void showError(const QString& message, const QString& infoText = "", + const QString& detailText = ""); + + void showInfo(const QString& message, const QString& infoText = "", + const QString& detailText = "", bool withIcon = false); + + void showInvalidFile(QString file, const exception& e); + + void showStatus(const QString& text, bool temporary = false); + + void updateMoveNumber(); + + void updateWindow(bool currentNodeChanged); + + void updateWindowModified(); + + void updateComment(); + + void updateMoveAnnotationActions(); + + void loadHistory(); + + void updateRecentFiles(); + + void updateFlipActions(); + + bool writeGame(const string& file); + +private slots: + void analyzeGameFinished(); + + void badMove(bool checked); + + void commentChanged(); + + void continueRatedGame(); + + void coordinates(bool checked); + + void doubtfulMove(bool checked); + + void fullscreen(); + + void genMoveFinished(); + + void goodMove(bool checked); + + void interestingMove(bool checked); + + void leaveFullscreen(); + + void levelTriggered(bool checked); + + void noMoveAnnotation(bool checked); + + void openCheckSave(const QString& file); + + void openRecentFile(); + + void orientationDisplayColorClicked(Color c); + + void rememberFile(const QString& file); + + void rememberDir(const QString& file); + + void selectNamedPiece(); + + void setMoveMarkingAllNumber(bool checked); + + void setMoveMarkingLastNumber(bool checked); + + void setMoveMarkingLastDot(bool checked); + + void setMoveMarkingNone(bool checked); + + void setSetupPlayer(); + + void setTitleMenuLevel(); + + void setupMode(bool checked); + + void showComment(bool checked); + + void toolBarNoText(bool checked); + + void toolBarText(const QString& key, Qt::ToolButtonStyle style); + + void toolBarTextBesideIcons(bool checked); + + void toolBarTextBelowIcons(bool checked); + + void toolBarTextOnly(bool checked); + + void toolBarTextSystem(bool checked); + + void veryBadMove(bool checked); + + void veryGoodMove(bool checked); + + void variantTriggered(bool checked); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_MAIN_WINDOW_H diff --git a/src/pentobi/RatedGamesList.cpp b/src/pentobi/RatedGamesList.cpp new file mode 100644 index 0000000..ef4a51b --- /dev/null +++ b/src/pentobi/RatedGamesList.cpp @@ -0,0 +1,127 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatedGamesList.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RatedGamesList.h" + +#include +#include +#include +#include "libboardgame_util/Log.h" +#include "libpentobi_gui/Util.h" + +//----------------------------------------------------------------------------- + +RatedGamesList::RatedGamesList(QWidget* parent) + : QTableView(parent) +{ + verticalHeader()->setVisible(false); + setShowGrid(false); + setEditTriggers(QAbstractItemView::NoEditTriggers); + setTabKeyNavigation(false); + setSelectionBehavior(QAbstractItemView::SelectRows); + setAlternatingRowColors(true); + m_model = new QStandardItemModel(this); + setModel(m_model); + connect(this, SIGNAL(doubleClicked(const QModelIndex&)), + SLOT(activateGame(const QModelIndex&))); +} + +void RatedGamesList::activateGame(const QModelIndex& index) +{ + auto item = m_model->item(index.row(), 0); + if (! item) + return; + bool ok; + unsigned n = item->text().toUInt(&ok); + if (ok) + emit openRatedGame(n); +} + +void RatedGamesList::focusInEvent(QFocusEvent* event) +{ + // Select current index if list has focus + selectRow(currentIndex().row()); + scrollTo(currentIndex()); + QTableView::focusInEvent(event); +} + +void RatedGamesList::focusOutEvent(QFocusEvent* event) +{ + // Show selection only if list has focus + clearSelection(); + QTableView::focusOutEvent(event); +} + +void RatedGamesList::keyPressEvent(QKeyEvent* event) +{ + if (event->type() == QEvent::KeyPress + && static_cast(event)->key() == Qt::Key_Space) + { + QModelIndexList indexes = + selectionModel()->selection().indexes(); + if (! indexes.isEmpty()) + activateGame(indexes[0]); + return; + } + QTableView::keyPressEvent(event); +} + +void RatedGamesList::updateContent(Variant variant, + const RatingHistory& history) +{ + m_model->clear(); + QStringList headers; + headers << tr("Game") << tr("Your Color") << tr("Level") << tr("Result") + << tr("Date"); + m_model->setHorizontalHeaderLabels(headers); + auto header = horizontalHeader(); + header->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); + header->setHighlightSections(false); + header->setSectionResizeMode(QHeaderView::ResizeToContents); + header->setStretchLastSection(true); + int nuRows = 0; + if (history.getGameInfos().size() + <= static_cast(numeric_limits::max())) + nuRows = static_cast(history.getGameInfos().size()); + m_model->setRowCount(nuRows); + setSortingEnabled(false); + for (int i = 0; i < nuRows; ++i) + { + auto& info = history.getGameInfos()[i]; + auto number = new QStandardItem; + number->setData(info.number, Qt::DisplayRole); + auto color = new QStandardItem; + if (info.color.to_int() < get_nu_colors(variant)) + color->setText(Util::getPlayerString(variant, info.color)); + else + LIBBOARDGAME_LOG("Error: invalid color in rating history"); + auto level = new QStandardItem; + level->setData(info.level, Qt::DisplayRole); + QString result; + if (info.result == 1) + result = tr("Win"); + else if (info.result == 0.5) + result = tr("Tie"); + else if (info.result == 0) + result = tr("Loss"); + int row = nuRows - i - 1; + m_model->setItem(row, 0, number); + m_model->setItem(row, 1, color); + m_model->setItem(row, 2, level); + m_model->setItem(row, 3, new QStandardItem(result)); + m_model->setItem(row, 4, new QStandardItem(info.date)); + } + setSortingEnabled(true); + if (nuRows > 0) + selectionModel()->setCurrentIndex(model()->index(0, 0), + QItemSelectionModel::NoUpdate); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/RatedGamesList.h b/src/pentobi/RatedGamesList.h new file mode 100644 index 0000000..8426850 --- /dev/null +++ b/src/pentobi/RatedGamesList.h @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatedGamesList.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_RATED_GAMES_LIST +#define PENTOBI_RATED_GAMES_LIST + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "RatingHistory.h" + +class QStandardItemModel; + +//----------------------------------------------------------------------------- + +class RatedGamesList + : public QTableView +{ + Q_OBJECT + +public: + explicit RatedGamesList(QWidget* parent = nullptr); + + void updateContent(Variant variant, const RatingHistory& history); + +signals: + void openRatedGame(unsigned n); + +protected: + void focusInEvent(QFocusEvent* event) override; + + void focusOutEvent(QFocusEvent* event) override; + + void keyPressEvent(QKeyEvent* event) override; + +private: + QStandardItemModel* m_model; + +private slots: + void activateGame(const QModelIndex& index); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_RATED_GAMES_LIST diff --git a/src/pentobi/RatingDialog.cpp b/src/pentobi/RatingDialog.cpp new file mode 100644 index 0000000..40edf5b --- /dev/null +++ b/src/pentobi/RatingDialog.cpp @@ -0,0 +1,172 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingDialog.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RatingDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Util.h" + +//----------------------------------------------------------------------------- + +QLabel* createSelectableLabel() +{ + auto label = new QLabel; + label->setTextInteractionFlags(Qt::TextSelectableByMouse); + return label; +} + +//----------------------------------------------------------------------------- + +RatingDialog::RatingDialog(QWidget* parent, RatingHistory& history) + : QDialog(parent), + m_history(history) +{ + setWindowTitle(tr("Rating")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto layout = new QVBoxLayout; + setLayout(layout); + auto formLayout = new QFormLayout; + layout->addLayout(formLayout); + formLayout->setLabelAlignment(Qt::AlignLeft); + auto box = new QHBoxLayout; + m_labelRating = createSelectableLabel(); + box->addWidget(m_labelRating); + box->addStretch(); + formLayout->addRow(tr("Your rating:"), box); + m_labelVariant = createSelectableLabel(); + formLayout->addRow(tr("Game variant:"), m_labelVariant); + m_labelNuGames = createSelectableLabel(); + formLayout->addRow(tr("Number rated games:"), m_labelNuGames); + m_labelBestRating = createSelectableLabel(); + formLayout->addRow(tr("Best previous rating:"), m_labelBestRating); + layout->addSpacing(layout->margin()); + layout->addWidget(new QLabel(tr("Recent development:"))); + m_graph = new RatingGraph; + layout->addWidget(m_graph, 1); + layout->addSpacing(layout->margin()); + layout->addWidget(new QLabel(tr("Recent games:"))); + m_list = new RatedGamesList; + layout->addWidget(m_list, 1); + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); + layout->addWidget(buttonBox); + m_clearButton = + buttonBox->addButton(tr("&Clear"), QDialogButtonBox::ActionRole); + buttonBox->button(QDialogButtonBox::Close)->setDefault(true); + buttonBox->button(QDialogButtonBox::Close)->setAutoDefault(true); + buttonBox->button(QDialogButtonBox::Close)->setFocus(); + updateContent(); + connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); + connect(buttonBox, SIGNAL(clicked(QAbstractButton*)), + SLOT(buttonClicked(QAbstractButton*))); + connect(m_list, SIGNAL(openRatedGame(unsigned)), + SLOT(activateGame(unsigned))); +} + +void RatingDialog::activateGame(unsigned n) +{ + emit openRecentFile(m_history.getFile(n)); +} + +void RatingDialog::buttonClicked(QAbstractButton* button) +{ + if (button != static_cast(m_clearButton)) + return; + QMessageBox msgBox(QMessageBox::Warning, "", + tr("Clear rating and delete rating history?"), + QMessageBox::Cancel, this); + Util::setNoTitle(msgBox); + auto clearButton = + msgBox.addButton(tr("Clear rating"), QMessageBox::DestructiveRole); + msgBox.setDefaultButton(clearButton); + msgBox.exec(); + if (msgBox.clickedButton() != clearButton) + return; + m_history.clear(); + updateContent(); +} + +void RatingDialog::updateContent() +{ + auto variant = m_history.getVariant(); + unsigned nuGames = m_history.getNuGames(); + Rating rating = m_history.getRating(); + Rating bestRating = m_history.getBestRating(); + if (nuGames == 0) + rating = Rating(0); + QString variantStr; + switch (variant) + { + case Variant::classic: + variantStr = tr("Classic (4 players)"); + break; + case Variant::classic_2: + variantStr = tr("Classic (2 players)"); + break; + case Variant::classic_3: + variantStr = tr("Classic (3 players)"); + break; + case Variant::duo: + variantStr = tr("Duo"); + break; + case Variant::trigon: + variantStr = tr("Trigon (4 players)"); + break; + case Variant::trigon_2: + variantStr = tr("Trigon (2 players)"); + break; + case Variant::trigon_3: + variantStr = tr("Trigon (3 players)"); + break; + case Variant::junior: + variantStr = tr("Junior"); + break; + case Variant::nexos: + variantStr = tr("Nexos (4 players)"); + break; + case Variant::nexos_2: + variantStr = tr("Nexos (2 players)"); + break; + case Variant::callisto: + variantStr = tr("Callisto (4 players)"); + break; + case Variant::callisto_2: + variantStr = tr("Callisto (2 players)"); + break; + case Variant::callisto_3: + variantStr = tr("Callisto (3 players)"); + break; + } + m_labelVariant->setText(variantStr); + m_labelNuGames->setText(QString::number(nuGames)); + if (nuGames == 0) + { + m_labelRating->setText("--"); + m_labelBestRating->setText("--"); + } + else + { + m_labelRating->setText(QString("%1").arg(rating.to_int())); + m_labelBestRating->setNum(bestRating.to_int()); + } + m_graph->updateContent(m_history); + m_list->updateContent(variant, m_history); + m_clearButton->setEnabled(nuGames > 0); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/RatingDialog.h b/src/pentobi/RatingDialog.h new file mode 100644 index 0000000..195409c --- /dev/null +++ b/src/pentobi/RatingDialog.h @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingDialog.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_RATING_DIALOG_H +#define PENTOBI_RATING_DIALOG_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "RatedGamesList.h" +#include "RatingGraph.h" +#include "libpentobi_base/Variant.h" + +class QAbstractButton; +class QLabel; + +using namespace std; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class RatingDialog + : public QDialog +{ + Q_OBJECT + +public: + /** Constructor. + @param parent + @param history (@ref libboardgame_doc_storesref) */ + RatingDialog(QWidget* parent, RatingHistory& history); + + void updateContent(); + +signals: + void openRecentFile(const QString& file); + +private: + RatingHistory& m_history; + + QPushButton* m_clearButton; + + QLabel* m_labelVariant; + + QLabel* m_labelNuGames; + + QLabel* m_labelRating; + + QLabel* m_labelBestRating; + + RatingGraph* m_graph; + + RatedGamesList* m_list; + +private slots: + void activateGame(unsigned n); + + void buttonClicked(QAbstractButton*); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_RATING_DIALOG_H diff --git a/src/pentobi/RatingGraph.cpp b/src/pentobi/RatingGraph.cpp new file mode 100644 index 0000000..c01d4f7 --- /dev/null +++ b/src/pentobi/RatingGraph.cpp @@ -0,0 +1,121 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingGraph.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RatingGraph.h" + +#include +#include +#include +#include + +//----------------------------------------------------------------------------- + +RatingGraph::RatingGraph(QWidget* parent) + : QFrame(parent) +{ + setMinimumSize(200, 60); + setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); +} + +void RatingGraph::paintEvent(QPaintEvent* event) +{ + QFrame::paintEvent(event); + QRect contentsRect = QFrame::contentsRect(); + int width = contentsRect.width(); + int height = contentsRect.height(); + QPainter painter(this); + painter.translate(contentsRect.x(), contentsRect.y()); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(255, 255, 255)); + painter.drawRect(0, 0, width, height); + if (! m_values.empty()) + { + QFontMetrics metrics(painter.font()); + float yRange = m_yMax - m_yMin; + float yTic = m_yMin; + float topMargin = ceil(1.2f * static_cast(metrics.height())); + float bottomMargin = ceil(0.3f * static_cast(metrics.height())); + float graphHeight = + static_cast(height) - topMargin - bottomMargin; + QPen pen(QColor(96, 96, 96)); + pen.setStyle(Qt::DotLine); + painter.setPen(pen); + int maxLabelWidth = 0; + while (yTic <= m_yMax) + { + int y = + static_cast(round( + topMargin + + graphHeight - (yTic - m_yMin) / yRange * graphHeight)); + painter.drawLine(0, y, width, y); + QString label; + label.setNum(yTic, 'f', 0); + int labelWidth = metrics.width(label + " "); + maxLabelWidth = max(maxLabelWidth, labelWidth); + painter.drawText(width - labelWidth, y - metrics.descent(), + label); + if (yRange < 600) + yTic += 100; + else + yTic += 200; + } + qreal dX = qreal(width - maxLabelWidth) / RatingHistory::maxGames; + qreal x = 0; + QPainterPath path; + for (unsigned i = 0; i < m_values.size(); ++i) + { + qreal y = + topMargin + + graphHeight - (m_values[i] - m_yMin) / yRange * graphHeight; + if (i == 0) + path.moveTo(x, y); + else + path.lineTo(x, y); + x += dX; + } + painter.setPen(Qt::red); + painter.setBrush(Qt::NoBrush); + painter.drawPath(path); + } +} + +QSize RatingGraph::sizeHint() const +{ + auto geo = QApplication::desktop()->screenGeometry(); + return QSize(geo.width() / 3, min(geo.width() / 12, geo.height() / 3)); +} + +void RatingGraph::updateContent(const RatingHistory& history) +{ + m_values.clear(); + auto& games = history.getGameInfos(); + if (games.empty()) + { + update(); + return; + } + m_yMin = games[0].rating.get(); + m_yMax = m_yMin; + for (const RatingHistory::GameInfo& info : games) + { + float rating = info.rating.get(); + m_yMin = min(m_yMin, rating); + m_yMax = max(m_yMax, rating); + m_values.push_back(rating); + } + m_yMin = floor((m_yMin / 100.f)) * 100; + m_yMax = ceil((m_yMax / 100.f)) * 100; + if (m_yMax == m_yMin) + m_yMax = m_yMin + 100; + update(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/RatingGraph.h b/src/pentobi/RatingGraph.h new file mode 100644 index 0000000..b737c9a --- /dev/null +++ b/src/pentobi/RatingGraph.h @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingGraph.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_RATING_GRAPH_H +#define PENTOBI_RATING_GRAPH_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "RatingHistory.h" + +//----------------------------------------------------------------------------- + +class RatingGraph + : public QFrame +{ + Q_OBJECT + +public: + explicit RatingGraph(QWidget* parent = nullptr); + + void updateContent(const RatingHistory& history); + + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + float m_yMin; + + float m_yMax; + + vector m_values; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_RATING_GRAPH_H diff --git a/src/pentobi/RatingHistory.cpp b/src/pentobi/RatingHistory.cpp new file mode 100644 index 0000000..5a27f62 --- /dev/null +++ b/src/pentobi/RatingHistory.cpp @@ -0,0 +1,194 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingHistory.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RatingHistory.h" + +#include +#include +#include +#include +#include +#include +#include "Util.h" +#include "libpentobi_base/PentobiTreeWriter.h" +#include "libpentobi_mcts/Player.h" + +using libpentobi_base::to_string_id; +using libpentobi_base::PentobiTreeWriter; +using libpentobi_mcts::Player; + +//----------------------------------------------------------------------------- + +namespace { + +/** 1000 Elo represents a beginner level. */ +const float startRating = 1000; + +QString getRatedGamesDir(Variant variant) +{ + return + Util::getDataDir() + "/rated_games/" + QString(to_string_id(variant)); +} + +} // namespace + +//----------------------------------------------------------------------------- + +RatingHistory::RatingHistory(Variant variant) +{ + load(variant); +} + +void RatingHistory::addGame(float score, Rating opponentRating, + unsigned nuOpponents, Color color, + float result, const QString& date, int level, + const PentobiTree& tree) +{ + float kValue = (m_nuGames < 30 ? 40.f : 20.f); + m_rating.update(score, opponentRating, kValue, nuOpponents); + if (m_rating.get() > m_bestRating.get()) + m_bestRating = m_rating; + ++m_nuGames; + GameInfo info; + info.number = m_nuGames; + info.color = color; + info.result = result; + info.date = date; + info.level = level; + info.rating = m_rating; + m_games.push_back(info); + size_t nuGames = m_games.size(); + if (nuGames > maxGames) + m_games.erase(m_games.begin(), m_games.begin() + nuGames - maxGames); + save(); + ofstream out(getFile(m_nuGames).toLocal8Bit().constData()); + PentobiTreeWriter writer(out, tree); + writer.set_indent(1); + writer.write(); + // Only save the last RatingHistory::maxGames games + if (m_nuGames > maxGames) + QFile::remove(getFile(m_nuGames - maxGames)); +} + +void RatingHistory::clear() +{ + QString variantStr = QString(to_string_id(m_variant)); + QSettings settings; + settings.remove("rated_games_" + variantStr); + settings.remove("rating_" + variantStr); + settings.remove("best_rating_" + variantStr); + for (const RatingHistory::GameInfo& info : getGameInfos()) + QFile::remove(getFile(info.number)); + QFile::remove(m_file); + m_nuGames = 0; + m_rating = Rating(startRating); + m_bestRating = Rating(startRating); + m_games.clear(); +} + +QString RatingHistory::getFile(unsigned n) const +{ + return QString("%1/%2.blksgf").arg(m_dir, QString::number(n)); +} + +void RatingHistory::getNextRatedGameSettings(int maxLevel, unsigned random, + int& level, Color& color) +{ + color = + Color(static_cast(random % get_nu_players(m_variant))); + float minDiff = 0; // Initialize to avoid compiler warning + for (int i = 1; i <= maxLevel; ++i) + { + float diff = + abs(m_rating.get() - Player::get_rating(m_variant, i).get()); + if (i == 1 || diff < minDiff) + { + minDiff = diff; + level = i; + } + } +} + +void RatingHistory::init(Rating rating) +{ + m_rating = rating; + m_bestRating = rating; + m_nuGames = 0; + m_games.clear(); + save(); +} + +void RatingHistory::load(Variant variant) +{ + m_variant = variant; + QString variantStr = QString(to_string_id(variant)); + QSettings settings; + m_nuGames = settings.value("rated_games_" + variantStr, 0).toUInt(); + // Default value is 1000 (Elo-rating for beginner-level play) + m_rating = + Rating(settings.value("rating_" + variantStr, startRating).toFloat()); + m_bestRating = + Rating(settings.value("best_rating_" + variantStr, 0).toFloat()); + m_games.clear(); + m_dir = getRatedGamesDir(variant); + m_file = m_dir + "/history.dat"; + ifstream file(m_file.toLocal8Bit().constData()); + if (! file) + return; + string line; + while (getline(file, line) && m_games.size() < maxGames) + { + istringstream in(line); + GameInfo info; + unsigned c; + string date; + in >> info.number >> c >> info.result >> date >> info.level + >> info.rating; + info.date = QString(date.c_str()); + if (! in || c >= get_nu_colors(variant)) + return; + info.color = Color(static_cast(c)); + if (info.number >= 1 && info.number <= m_nuGames) + m_games.push_back(info); + } + size_t nuGames = m_games.size(); + if (nuGames > maxGames) + m_games.erase(m_games.begin(), m_games.begin() + nuGames - maxGames); + // Make the all-time best rating consistent with the rating history. Older + // versions of Pentobi (up to version 3) did not save the all-time best + // rating, so after an upgrade to a newer version of Pentobi, the history + // of recent rated games can contain a higher rating than the stored + // all-time best rating. + for (const RatingHistory::GameInfo& info : getGameInfos()) + if (info.rating.get() > m_bestRating.get()) + m_bestRating = info.rating; +} + +void RatingHistory::save() const +{ + QString variantStr = QString(to_string_id(m_variant)); + QSettings settings; + settings.setValue("rated_games_" + variantStr, m_nuGames); + settings.setValue("rating_" + variantStr, + static_cast(m_rating.get())); + settings.setValue("best_rating_" + variantStr, + static_cast(m_bestRating.get())); + LIBBOARDGAME_ASSERT(! m_file.isEmpty()); + QDir dir(""); + dir.mkpath(m_dir); + ofstream out(m_file.toLocal8Bit().constData()); + for (auto& info : m_games) + out << info.number << ' ' << static_cast(info.color.to_int()) + << ' ' << info.result << ' ' + << info.date.toLocal8Bit().constData() << ' ' << info.level + << ' ' << info.rating << '\n'; +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/RatingHistory.h b/src/pentobi/RatingHistory.h new file mode 100644 index 0000000..568fe5a --- /dev/null +++ b/src/pentobi/RatingHistory.h @@ -0,0 +1,140 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingHistory.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_RATING_HISTORY_H +#define PENTOBI_RATING_HISTORY_H + +#include +#include +#include "libboardgame_base/Rating.h" +#include "libpentobi_base/Color.h" +#include "libpentobi_base/PentobiTree.h" +#include "libpentobi_base/Variant.h" + +using namespace std; +using libboardgame_base::Rating; +using libpentobi_base::Color; +using libpentobi_base::PentobiTree; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** History of rated games in a certain game variant. */ +class RatingHistory +{ +public: + /** Maximum number of games to remember in the history. */ + static const unsigned maxGames = 100; + + struct GameInfo + { + /** Game number. + The first game played has number 0. */ + unsigned number; + + /** Color played by the human. + In game variants with multiple colors per player, the human played + all colors played by the player of this color. */ + Color color; + + /** Game result. + 0=Loss, 0.5=tie, 1=win from the viewpoint of the human. */ + float result; + + /** Date of the game in "YYYY-MM-DD" format. */ + QString date; + + /** The playing level of the computer opponent. */ + int level; + + /** The rating of the human after the game. */ + Rating rating; + }; + + + explicit RatingHistory(Variant variant); + + /** Initialize rating to a given a-priori value. */ + void init(Rating rating); + + /** Get level and user color for next rated games. + @param maxLevel The maximum playing level. + @param random A random number to determine the color for the human. + @param[out] level The playing level for the next game. + @param[out] color The color for the human in the next game. */ + void getNextRatedGameSettings(int maxLevel, unsigned random, int& level, + Color& color); + + /** Append a new game. */ + void addGame(float score, Rating opponentRating, unsigned nuOpponents, + Color color, float result, const QString& date, int level, + const PentobiTree& tree); + + /** Get file name of the n'th rated game. */ + QString getFile(unsigned n) const; + + void load(Variant variant); + + /** Saves the history. */ + void save() const; + + const vector& getGameInfos() const; + + Variant getVariant() const; + + const Rating& getRating() const; + + const Rating& getBestRating() const; + + unsigned getNuGames() const; + + void clear(); + +private: + Variant m_variant; + + Rating m_rating; + + unsigned m_nuGames; + + Rating m_bestRating; + + QString m_dir; + + QString m_file; + + vector m_games; +}; + +inline const vector& RatingHistory::getGameInfos() + const +{ + return m_games; +} + +inline unsigned RatingHistory::getNuGames() const +{ + return m_nuGames; +} + +inline const Rating& RatingHistory::getBestRating() const +{ + return m_bestRating; +} + +inline const Rating& RatingHistory::getRating() const +{ + return m_rating; +} + +inline Variant RatingHistory::getVariant() const +{ + return m_variant; +} + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_RATING_HISTORY_H diff --git a/src/pentobi/ShowMessage.cpp b/src/pentobi/ShowMessage.cpp new file mode 100644 index 0000000..b8e19f7 --- /dev/null +++ b/src/pentobi/ShowMessage.cpp @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/ShowMessage.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "ShowMessage.h" + +#include +#include "Util.h" + +//----------------------------------------------------------------------------- + +namespace { + +void showMessage(QWidget* parent, QMessageBox::Icon icon, const QString& text, + const QString& infoText, const QString& detailText) +{ + QMessageBox msgBox(parent); + Util::setNoTitle(msgBox); + msgBox.setIcon(icon); + msgBox.setText(text); + msgBox.setInformativeText(infoText); + msgBox.setDetailedText(detailText); + msgBox.exec(); +} + +} // namespace + +//----------------------------------------------------------------------------- + +void initQuestion(QMessageBox& msgBox, const QString& text, + const QString& infoText) +{ + Util::setNoTitle(msgBox); + msgBox.setText(text); + msgBox.setInformativeText(infoText); +} + +void showFatal(const QString& detailedText) +{ + // Don't translate these error messages. They shouldn't occur if the + // program is correct and if it is not, they can occur in situations + // when the translators are not yet installed. + QMessageBox msgBox; + msgBox.setWindowTitle("Pentobi"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText("An unexpected error occurred."); + QString infoText = + "Please report this error together with any details available with" + " the button below and other context information at the Pentobi" + " bug tracker."; + msgBox.setInformativeText("" + infoText); + msgBox.setDetailedText(detailedText); + msgBox.exec(); +} + +void showError(QWidget* parent, const QString& text, const QString& infoText, + const QString& detailText) +{ + showMessage(parent,QMessageBox::Critical, text, infoText, detailText); +} + +void showInfo(QWidget* parent, const QString& text, const QString& infoText, + const QString& detailText, bool withIcon) +{ + showMessage(parent, + withIcon ? QMessageBox::Information : QMessageBox::NoIcon, + text, infoText, detailText); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/ShowMessage.h b/src/pentobi/ShowMessage.h new file mode 100644 index 0000000..3cc3cdc --- /dev/null +++ b/src/pentobi/ShowMessage.h @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/ShowMessage.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_SHOW_MESSAGE_H +#define PENTOBI_SHOW_MESSAGE_H + +#include + +class QMessageBox; +class QWidget; + +//----------------------------------------------------------------------------- + +void initQuestion(QMessageBox& msgBox, const QString& text, + const QString& infoText = ""); + +void showError(QWidget* parent, const QString& text, + const QString& infoText = "", const QString& detailText = ""); + +void showInfo(QWidget* parent, const QString& text, + const QString& infoText = "", const QString& detailText = "", + bool withIcon = false); + +void showFatal(const QString& detailedText); + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_SHOW_MESSAGE_H diff --git a/src/pentobi/Util.cpp b/src/pentobi/Util.cpp new file mode 100644 index 0000000..387b7a2 --- /dev/null +++ b/src/pentobi/Util.cpp @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Util.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Util.h" + +#include +#include +#include +#include +#include +#include +#include +#include "libpentobi_mcts/Player.h" + +using libpentobi_mcts::Player; + +//----------------------------------------------------------------------------- + +namespace Util +{ + +QString getDataDir() +{ + return QStandardPaths::writableLocation(QStandardPaths::DataLocation); +} + +void initDataDir() +{ + QString dataLocation = getDataDir(); + QDir dir(dataLocation); + if (! dir.exists()) + // Note: dataLocation is an absolute path but there is no static + // function QDir::mkpath() + dir.mkpath(dataLocation); +} + +void removeThumbnail(const QString& file) +{ + // Note: in the future, it might be possible to trigger a thumbnail + // update via D-Bus instead of removing it, but this is not yet + // implemented in Gnome + QFileInfo info(file); + QString canonicalFile = info.canonicalFilePath(); + if (canonicalFile.isEmpty()) + canonicalFile = info.absoluteFilePath(); + QByteArray url = QUrl::fromLocalFile(canonicalFile).toEncoded(); + QByteArray md5 = + QCryptographicHash::hash(url, QCryptographicHash::Md5).toHex(); + QString home = QDir::home().path(); + QFile::remove(home + "/.thumbnails/normal/" + md5 + ".png"); + QFile::remove(home + "/.thumbnails/large/" + md5 + ".png"); +} + +void setNoTitle(QDialog& dialog) +{ + // On many platforms, message boxes should have no title but using + // an emtpy string causes Qt to use the lower-case application name (tested + // on Linux with Qt 4.8). As a workaround, we set the title to a space + // character. + dialog.setWindowTitle(" "); +} + +} // namespace Util + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/Util.h b/src/pentobi/Util.h new file mode 100644 index 0000000..d5ba8cc --- /dev/null +++ b/src/pentobi/Util.h @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Util.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_UTIL_H +#define PENTOBI_UTIL_H + +#include "RatingHistory.h" +#include "libboardgame_base/Rating.h" +#include "libpentobi_base/Color.h" +#include "libpentobi_base/Variant.h" + +class QDialog; +class QString; + +using libboardgame_base::Rating; +using libpentobi_base::Color; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +namespace Util +{ + +/** Remove a thumbnail for a given file. + Currently, the QT open file dialog shows thumbnails even if they belong + to old versions of a file (see QTBUG-24724). This function can be used + to remove an out-of-date freedesktop.org thumbnail if we know a file has + changed (e.g. after saving). */ +void removeThumbnail(const QString& file); + +/** Return the platform-dependent directory for storing data for the current + application. */ +QString getDataDir(); + +/** Create the platform-dependent directory for storing data for the current + application if it does not exist yet. */ +void initDataDir(); + +/** Set an empty window title for message boxes and similar small dialogs. */ +void setNoTitle(QDialog& dialog); + +} + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_UTIL_H diff --git a/src/pentobi/help/C/pentobi/analysis.jpg b/src/pentobi/help/C/pentobi/analysis.jpg new file mode 100644 index 0000000..8731db4 Binary files /dev/null and b/src/pentobi/help/C/pentobi/analysis.jpg differ diff --git a/src/pentobi/help/C/pentobi/become_stronger.html b/src/pentobi/help/C/pentobi/become_stronger.html new file mode 100644 index 0000000..e1f203f --- /dev/null +++ b/src/pentobi/help/C/pentobi/become_stronger.html @@ -0,0 +1,60 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Become a Stronger Player

+

Pentobi has functionality that can help you to become a stronger Blokus +player.

+

Game Analysis

+

A game can be analyzed by selecting Analyze Game from the +Tools menu. This will make the computer player evaluate each position in +the main variation. The result is displayed in a window with a diagram of +colored dots.

+

Game analysis window

+
Analysis of a game of variant Classic (2 +players).
+

Each dot represents a game position in which the color of the dot was to +play. The dots are ordered horizontally by move number. The vertical axis +represents the estimated probability of winning the game for the color to play. +Mouse clicks in the diagram will go to the corresponding position.

+

The position values are only estimates and the computer will sometimes +evaluate positions incorrectly. But sudden drops in the value can help you find +moves that were potentially bad. You can go back to the position before the +move and try to find a better move or ask the computer what it would have +played by selecting Play Single Move from the Computer menu.

+

Determine Your Rating

+

You can track your progress by playing rated games against the computer. The +game results are used to determine your current rating. The rating is a number +that represents your playing strength.

+

A rated game is started with Rated Game from the Game menu or +the toolbar. If you have not played any rated games in the current game +variant, you will be asked to choose a start value, which can reduce the number +of games needed for determining your real rating. If you are a beginner, leave +the start value at 1000.

+

For each rated game, the computer will choose a playing level for the +computer opponent according to your current rating. The color you play will be +randomly chosen in each game.

+

During a rated game, most of the functions not needed for playing are +disabled: you cannot undo moves, navigate in the game, change the computer +colors or change the playing level. To get an accurate rating, you should +always play rated games until the end.

+

After the game has ended, your rating will be updated depending on the game +result and the computer level. For the game result, it only matters if the game +was won, lost or a tie. The exact number of score points does not matter.

+

Rating window

+
Window with rating graph.
+

You can always see your current rating by selecting Rating from the +Tools menu. This will open a window that shows the development of your +rating during the last 100 games as a graph. The last 100 games are +automatically saved and can be loaded by double-clicking on the rows in the +game table below the graph.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/board_callisto.png b/src/pentobi/help/C/pentobi/board_callisto.png new file mode 100644 index 0000000..f6e63bb Binary files /dev/null and b/src/pentobi/help/C/pentobi/board_callisto.png differ diff --git a/src/pentobi/help/C/pentobi/board_classic.png b/src/pentobi/help/C/pentobi/board_classic.png new file mode 100644 index 0000000..cbbd188 Binary files /dev/null and b/src/pentobi/help/C/pentobi/board_classic.png differ diff --git a/src/pentobi/help/C/pentobi/board_duo.png b/src/pentobi/help/C/pentobi/board_duo.png new file mode 100644 index 0000000..f41d71b Binary files /dev/null and b/src/pentobi/help/C/pentobi/board_duo.png differ diff --git a/src/pentobi/help/C/pentobi/board_nexos.png b/src/pentobi/help/C/pentobi/board_nexos.png new file mode 100644 index 0000000..65c55d0 Binary files /dev/null and b/src/pentobi/help/C/pentobi/board_nexos.png differ diff --git a/src/pentobi/help/C/pentobi/board_trigon.jpg b/src/pentobi/help/C/pentobi/board_trigon.jpg new file mode 100644 index 0000000..e75d89c Binary files /dev/null and b/src/pentobi/help/C/pentobi/board_trigon.jpg differ diff --git a/src/pentobi/help/C/pentobi/callisto_rules.html b/src/pentobi/help/C/pentobi/callisto_rules.html new file mode 100644 index 0000000..ef5a189 --- /dev/null +++ b/src/pentobi/help/C/pentobi/callisto_rules.html @@ -0,0 +1,45 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Callisto Rules

+

Callisto is a another board game similar to Blokus. The board is derived +from the classic 20×20 Blokus board by removing the corners such that an +octagon with a top edge of size six remains. The pieces are a subset of the +polyominoes up to size five. They include three 1×1 pieces per player that play +a special role.

+

+"Pieces

+
The 21 pieces.
+

The 1×1 pieces may be placed anywhere on the board apart from the center of +the board. The center consists of an octagon with width six and top edge size +two. The first two moves of a player must use a 1×1 piece, the third 1×1 piece +may be played anytime later.

+

+"Board

+
The board with the center having a darker +color.
+

All larger pieces may be placed anwhere on the board but must touch an +existing piece of the same color edge-to-edge.

+

+"Example

+
An example position after a few +moves.
+

The score of a color is the number of squares on the board occupied by the +color not counting 1×1 pieces. Bonus points are not used. Unlike in Blokus, +ties are broken in favor of the player who started later.

+

Rules for two or three players

+

The game can be played with less than four players by using a smaller board. +For three players, the board is an octagon with width 20 and top edge size two. +For two players, the board is an octagon with width 17 and top edge size two. +The size of the center stays the same.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/classic_rules.html b/src/pentobi/help/C/pentobi/classic_rules.html new file mode 100644 index 0000000..1b6f1c4 --- /dev/null +++ b/src/pentobi/help/C/pentobi/classic_rules.html @@ -0,0 +1,63 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Classic Rules

+

There are four players, Blue, Yellow, Red and Green, and a board consisting +of 20×20 squares.

+

Each player has a set of 21 pieces of his color shaped like the polyominoes +up to size five. (A polyomino is a shape built by a number of squares connected +along the edges.)

+

+"Pieces

+
The 21 pieces.
+

The players alternate in placing one of their pieces on the board. Blue +starts, followed by Yellow, then Red, then Green.

+

Each player has a starting square. Blue's starting square is in the top left +corner, Yellow's in the top right corner, Red's in the bottom right corner and +Green's in the bottom left corner. The first piece of a player must cover its +starting square.

+

+"Board

+
The 20×20 board with the starting
+squares marked with colored dots.
+

The following pieces must be placed on empty squares such that the new piece +touches at least one piece of its own color corner-to-corner but does not touch +any piece of its own color along the edges. The new piece may touch edges of +pieces of the opponent colors.

+

+"Example

+
An example position after a few +moves.
+

When the player of a color cannot place any more pieces, the player passes +and the next color continues.

+

When none of the players can place any more pieces, the player with the +highest score wins. The score of a color is the number of squares on the board +occupied by the color, plus a bonus of 15 points if the color could place all +of its pieces, plus an additional bonus of 5 points if the color could place +all pieces and the last piece played was the one-square piece.

+

Rules for Two Players

+

The game can be played with two players. The first player plays both Blue +and Red, the second player Yellow and Green. The points of both colors played +by a player are added up.

+

Rules for Three Players

+

The game can also be played with three players. The players take turns +playing the fourth color (Green). At the end of the game, the score of Green is +ignored.

+

Colorless starting points

+

Note that the original Blokus Classic rules used colorless starting points. +This means that each color may freely choose, which of the remaining unoccupied +starting points to use for its first move. Pentobi currently only supports the +rule variant with colored starting points because this rule variant was used on +the Blokus online server at blokus.com and in most of the past Blokus +tournaments.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/duo_rules.html b/src/pentobi/help/C/pentobi/duo_rules.html new file mode 100644 index 0000000..53dba8d --- /dev/null +++ b/src/pentobi/help/C/pentobi/duo_rules.html @@ -0,0 +1,28 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Duo Rules

+

The game variant Duo is another game variant for two players. The game is +played on a smaller board with 14×14 squares. There is only one color per +player (Blue and Green) and the starting squares are not in the corners, but on +the square with the coordinates (5,10) for Blue, and on (10,5) for Green.

+

+"Board

+
The 14×14 board used in game variant Duo +with
+the starting squares marked with colored dots.
+

+"Example

+
An example position in game variant +Duo.
+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/index.html b/src/pentobi/help/C/pentobi/index.html new file mode 100644 index 0000000..8e5329d --- /dev/null +++ b/src/pentobi/help/C/pentobi/index.html @@ -0,0 +1,28 @@ + + + +Pentobi Help + + + + +

Next

+

Pentobi

+

Pentobi is a computer opponent for the board game Blokus. In this game, four +players place pieces similar to the pieces of the computer game Tetris on a +20×20 board. Pentobi also supports the game variants for two or three players +and the game variants Duo, Trigon, Junior, Nexos and Callisto.

+

Classic Rules
+Duo Rules
+Trigon Rules
+Junior Rules
+Nexos Rules
+Callisto Rules
+How to Use Pentobi
+Become a Stronger Player
+The Window Menu
+Keyboard Shortcuts
+System Requirements
+License

+ + diff --git a/src/pentobi/help/C/pentobi/junior_rules.html b/src/pentobi/help/C/pentobi/junior_rules.html new file mode 100644 index 0000000..ce2ff38 --- /dev/null +++ b/src/pentobi/help/C/pentobi/junior_rules.html @@ -0,0 +1,22 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Junior Rules

+

Junior is a simplified game variant for two players. It is played on the +same 14×14 board as game variant Duo but uses only a subset of the pentominoes +and the players get two of each of those pentominoes.

+

+"Pieces

+
The 24 pieces used in Junior.
+

Bonus points are not used in Junior.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/license.html b/src/pentobi/help/C/pentobi/license.html new file mode 100644 index 0000000..6e97dcf --- /dev/null +++ b/src/pentobi/help/C/pentobi/license.html @@ -0,0 +1,26 @@ + + + +Pentobi Help + + + + +

Previous

+

License

+

Copyright © 2011–2017 Markus Enzenberger

+

This program is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) any +later version.

+

This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details.

+

Trademark Disclaimer

+

The trademark Blokus and other trademarks referred to are property of their +respective trademark holders. The trademark holders are not affiliated with the +author of the program Pentobi.

+

Previous

+ + diff --git a/src/pentobi/help/C/pentobi/nexos_rules.html b/src/pentobi/help/C/pentobi/nexos_rules.html new file mode 100644 index 0000000..5e51e81 --- /dev/null +++ b/src/pentobi/help/C/pentobi/nexos_rules.html @@ -0,0 +1,44 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Nexos Rules

+

Nexos is a board game similar to Blokus. The board is a rectangular 13×13 +line grid. Each color uses 24 pieces that consist of up to four connected line +segments.

+

Pieces for Nexos

+
The 24 pieces.
+

Each color has a starting intersection on the intersection of the third +lines close to a corner. The first piece must touch the starting +intersection.

+

Board for Nexos

+
The board for Nexos with segments touching +the
+starting intersections marked with colored dots.
+

The following pieces must be placed on empty line segments such that a +segment of the new piece touches an intersection that is already touched by a +segment of the same color. It does not matter if pieces of other colors touch +or cover the same intersection. However, pieces may not overlap. The junctions +between the segments within a piece are such that two rectangular junctions of +different pieces can cover the same intersection without overlapping, but +straight junctions cannot.

+

+"Example

+
An example position after a few +moves.
+

The score of a color is the number of line segments on the board covered by +the color, plus a bonus of 10 points if the color could place all of its +pieces.

+

Rules for Two Players

+

Like Blokus, Nexos can be played with two players by having one player play +Blue and Red and the other player Yellow and Green.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/pieces.png b/src/pentobi/help/C/pentobi/pieces.png new file mode 100644 index 0000000..4af2864 Binary files /dev/null and b/src/pentobi/help/C/pentobi/pieces.png differ diff --git a/src/pentobi/help/C/pentobi/pieces_callisto.png b/src/pentobi/help/C/pentobi/pieces_callisto.png new file mode 100644 index 0000000..976efdc Binary files /dev/null and b/src/pentobi/help/C/pentobi/pieces_callisto.png differ diff --git a/src/pentobi/help/C/pentobi/pieces_junior.png b/src/pentobi/help/C/pentobi/pieces_junior.png new file mode 100644 index 0000000..6940d2f Binary files /dev/null and b/src/pentobi/help/C/pentobi/pieces_junior.png differ diff --git a/src/pentobi/help/C/pentobi/pieces_nexos.png b/src/pentobi/help/C/pentobi/pieces_nexos.png new file mode 100644 index 0000000..9595666 Binary files /dev/null and b/src/pentobi/help/C/pentobi/pieces_nexos.png differ diff --git a/src/pentobi/help/C/pentobi/pieces_trigon.jpg b/src/pentobi/help/C/pentobi/pieces_trigon.jpg new file mode 100644 index 0000000..bc9457b Binary files /dev/null and b/src/pentobi/help/C/pentobi/pieces_trigon.jpg differ diff --git a/src/pentobi/help/C/pentobi/position_callisto.png b/src/pentobi/help/C/pentobi/position_callisto.png new file mode 100644 index 0000000..1d4d1bd Binary files /dev/null and b/src/pentobi/help/C/pentobi/position_callisto.png differ diff --git a/src/pentobi/help/C/pentobi/position_classic.png b/src/pentobi/help/C/pentobi/position_classic.png new file mode 100644 index 0000000..7b73e4b Binary files /dev/null and b/src/pentobi/help/C/pentobi/position_classic.png differ diff --git a/src/pentobi/help/C/pentobi/position_duo.png b/src/pentobi/help/C/pentobi/position_duo.png new file mode 100644 index 0000000..bc6c8e2 Binary files /dev/null and b/src/pentobi/help/C/pentobi/position_duo.png differ diff --git a/src/pentobi/help/C/pentobi/position_nexos.png b/src/pentobi/help/C/pentobi/position_nexos.png new file mode 100644 index 0000000..1d66da4 Binary files /dev/null and b/src/pentobi/help/C/pentobi/position_nexos.png differ diff --git a/src/pentobi/help/C/pentobi/position_trigon.jpg b/src/pentobi/help/C/pentobi/position_trigon.jpg new file mode 100644 index 0000000..6f6aedc Binary files /dev/null and b/src/pentobi/help/C/pentobi/position_trigon.jpg differ diff --git a/src/pentobi/help/C/pentobi/rating.jpg b/src/pentobi/help/C/pentobi/rating.jpg new file mode 100644 index 0000000..e92aa99 Binary files /dev/null and b/src/pentobi/help/C/pentobi/rating.jpg differ diff --git a/src/pentobi/help/C/pentobi/shortcuts.html b/src/pentobi/help/C/pentobi/shortcuts.html new file mode 100644 index 0000000..ff2e75d --- /dev/null +++ b/src/pentobi/help/C/pentobi/shortcuts.html @@ -0,0 +1,58 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Keyboard Shortcuts

+

In addition to the menu item shortcut keys, which are shown in the window +menu, the following shortcut keys are supported by Pentobi. Note that these +shortcuts are not active when the comment text field is shown and has the +focus. In this case, the focus can be switched away from the comment text with +the Tab key.

+
+
Plus
+
+

Select next piece

+
+
Minus
+
+

Select previous piece

+
+
0
+
+

Clear selected piece

+
+
Space
+
+

Next orientation of the selected piece

+
+
Shift+Space
+
+

Previous orientation of the selected piece

+
+
Left, Right, Up, Down
+
+

Move the selected piece.

+
+
Enter
+
+

Play the selected piece.

+
+
1, 2, A, C, E, F, G, H, I, J, L, N, O, P, S, T, U, V, W, X, Y, Z
+
+

Select piece according to commonly used piece names. If there are multiple +pieces with the letter (e.g. I3, I4, I5), pressing the key several times cycles +between them. Some letters are used only in certain game variants. For example, +A is used only in Trigon for the pieces A6 and A4 (also known as "lobster" and +"triangle").

+
+
+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/stylesheet.css b/src/pentobi/help/C/pentobi/stylesheet.css new file mode 100644 index 0000000..62522b0 --- /dev/null +++ b/src/pentobi/help/C/pentobi/stylesheet.css @@ -0,0 +1,20 @@ +body +{ + color: black; + background-color: white; + font-family: sans-serif; + font-size: 15px; + margin-left: 0.5em; + margin-right: 0.5em; + max-width: 60em; +} + +:link +{ + text-decoration: none; +} + +div.caption +{ + font-size: 14px; +} diff --git a/src/pentobi/help/C/pentobi/system.html b/src/pentobi/help/C/pentobi/system.html new file mode 100644 index 0000000..c01c0e4 --- /dev/null +++ b/src/pentobi/help/C/pentobi/system.html @@ -0,0 +1,22 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

System Requirements

+

Minimum: 1 GB RAM, 1 GHz CPU
+Recommended for playing level 9: 4 GB RAM, 2 GHz dual-core or faster +CPU

+

Pentobi will also work on systems that do not meet the minimum requirements +but the highest playing level will be very slow on those systems (if the CPU is +too slow) or have a reduced playing strength (if there is not enough +memory).

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/trigon_rules.html b/src/pentobi/help/C/pentobi/trigon_rules.html new file mode 100644 index 0000000..6668322 --- /dev/null +++ b/src/pentobi/help/C/pentobi/trigon_rules.html @@ -0,0 +1,44 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Trigon Rules

+

Trigon is another game variant. The rules a similar to game variant Classic +but it uses a differently shaped board and a different set of pieces. Each +color uses 22 pieces that are shaped like the polyiamonds up to size six. (A +polyiamond is a shape built by a number of equilateral triangles connected +along the edges.)

+

+"Pieces

+
The 22 Trigon pieces.
+

The board also consists of triangles and is shaped like a hexagon with an +edge size of nine triangles.

+

+"Board

+
The board with the starting
+fields marked with gray dots.
+

There are six starting points on the board, each located in the middle of +the fourth row away from each edge. The starting points are not colored and the +players may freely choose a starting point for the first piece of a color.

+

+"Example

+
An example position after a few +moves.
+

Rules for Two Players

+

Like game variant Classic, Trigon can be played with two players by having +one player play Blue and Red and the other player Yellow and Green.

+

Rules for Three Players

+

Trigon can be played with three players using the same rules as for the +four-player variant. The three-player variant is played on a smaller board with +an edge size of eight triangles. The starting points are located in the middle +of the third row away from each edge.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/user_interface.html b/src/pentobi/help/C/pentobi/user_interface.html new file mode 100644 index 0000000..1e0928d --- /dev/null +++ b/src/pentobi/help/C/pentobi/user_interface.html @@ -0,0 +1,72 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

How to Use Pentobi

+

Board

+

Pentobi's main window shows the board on the left side. The played pieces on +the board can have numbers on them that indicate the move number in which the +piece was played. An letter after the move number indicates that there exists a +variation to this move (see below).

+

Pieces can be played by moving them to a place that corresponds to a legal +move with the mouse or arrow keys and pressing the left mouse button or the +Enter key.

+

Pieces and Score

+

On the right side, the remaining pieces are shown. Above the remaining +pieces is an orientation selector that shows the currently selected piece and +allows the player to change its orientation. If no piece is selected and the +game has not yet ended, a colored dot in the orientation selector shows the +color to play.

+

Pieces can be selected by clicking on one of the remaining pieces shown, by +using the left/right arrow buttons in the orientation selector or by using +shortcut keys.

+

Below the orientation selector is a score display, which displays the +current points for each color or player. The points are the sum of on-board +points and bonus points. Points are underlined if they are final because the +color cannot play more pieces. A small star indicates that the points include a +bonus.

+

Playing Against the Computer

+

The board can be used for creating game records of games played by humans or +for playing games against the computer. In games against the computer, the +computer can play any (or several) of the colors.

+

When you start a new game, the human will play the color(s) of the first +player by default and the computer all other colors. To change this, use +Computer Colors from the Computer menu or toolbar and select the +colors the computer should play.

+

The exception is that the computer will play no color by default if it +played no color in the previous game. This prevents the computer from +automatically starting to play if the user mainly wants to use the board for +entering move sequences or similar editing tasks. So if you want to use the +board without playing against the computer, you need to disable the computer +colors in the Computer Colors dialog only once and it will stay that +way. After loading a saved game, the computer also plays no color by +default.

+

Selecting Play from the Computer menu or the toolbar always +makes the computer play a move for the current color. If the computer did not +already play this color before, it will also make the computer play this color +(and only this color) from now on.

+

Move Variations and the Game Tree

+

When you play a game, Pentobi will store the sequence of moves and it is +always possible to go back to a previous position and play differently. If you +do this, the new sequence is stored as an alternative sequence (called +variation). Variations can also be used by annotators for commenting on +existing games. Variations can exist at any board position and can have +subvariations themselves. The game can therefore become a game tree, in which +each node represents a board position. You can navigate in the game tree with +the items in the Go menu or in the toolbar.

+

The main variation is the sequence of moves that starts at the start +position and always selects the first child node in each position (e.g. by +selecting Forward in the Go menu or toolbar). The main variation +is supposed to represent the real game played. If you want a side variation to +become the main variation, select Make Main Variation from the +Edit menu.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/window_menu.html b/src/pentobi/help/C/pentobi/window_menu.html new file mode 100644 index 0000000..51c56c4 --- /dev/null +++ b/src/pentobi/help/C/pentobi/window_menu.html @@ -0,0 +1,201 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

The Window Menu

+

Game

+
+
New
+
Start a new game.
+
Rated Game
+
Start a new rated game against +the computer.
+
Game Variant
+
Select a game variant and start a new game of this game variant.
+
Game Info
+
Display or edit additional information about the game like the name of the +players or the date when the game was played.
+
Undo Move
+
Undo the last move played and remove it from the game tree. Undoing a move +is only possible if it is the last move in the current variation (i.e. a leaf +node in the game tree; use Edit/Truncate to remove inner nodes of the +game tree).
+
Find Move
+
Find a legal move for the current color and display it for a few seconds on +the board. Selecting this item repeatedly will show all legal moves.
+
Open
+
Load a saved game. The board position after loading will be the last +position in the main variation unless the game starts with a setup position. If +the game starts with a setup, the board position will be the first position +instead. This avoids that solutions are immediately shown if the file contains +a Blokus puzzle as a setup with the solution as the main variation.
+
Open Recent
+
Load a recently used game.
+
Save
+
Save the current game.
+
Save As
+
Save the current game under a new file name.
+
Export/Image
+
Save the current position as an image file. Several image file formats are +supported, the file format is derived from the file name ending (e.g. ".png" +for the PNG format).
+
Export/ASCII Art
+
Save the current position as a text diagram. The text diagram should be +viewed using a monospace font.
+
Quit
+
Quit Pentobi.
+
+

Go

+
+
Beginning
+
Go to the beginning of the game.
+
Backward
+
Go one move backward in the current variation. The corresponding button in +the toolbar supports autorepeat if pressed and held.
+
Forward
+
Go one move forward in the current variation. If the current position has +several follow-up variations (i.e. the current node in the game tree has +several child nodes), the first variation will be used. The corresponding +button in the toolbar supports autorepeat if pressed and held.
+
End
+
Go to the end of the current variation. Like Forward, this also uses +the first variation in positions with several follow-up variations.
+
Next Variation
+
Go to the next variation to the last move played (i.e. the next sibling +node of the current node in the game tree).
+
Previous Variation
+
Go to the previous variation to the last move played (i.e. the previous +sibling node of the current node in the game tree).
+
Go to Move
+
Go to the move with a given move number in the current variation.
+
Back to Main Variation
+
Go back to the last position in the current variation that belonged to the +main variation.
+
Beginning of Branch
+
Go back to the last position in the current variation that had an +alternative move.
+
Find Next Comment
+
Go to the next position that has a comment. If the comment text field was +not visible, it will become visible. Selecting this item repeatedly will show +all positions with comments in the game tree.
+
+

Edit

+
+
Move Annotation
+
Add a chess-style annotation symbol (e.g. !!) to the current move. The +symbols are appended to the move numbers in the status bar and, depending on +the configuration of Move Marking, on the board.
+
Make Main Variation
+
Make the current variation the main variation of the game. This reorders +the nodes in the game tree such that the current variation becomes the main +variation.
+
Move Variation Up
+
Changes the order of variations such that the current position will appear +earlier when iterating over the variations with Next/Previous +Variation.
+
Move Variation Down
+
Changes the order of variations such that the current position will appear +later when iterating over the variations with Next/Previous +Variation.
+
Delete All Variations
+
Delete all variations but the main variation. If the current position is +not in the main variation, it will first be changed to a position as in Back +to Main Variation.
+
Truncate
+
Remove the node with the current position, including any subtree, from the +game tree.
+
Truncate Children
+
Remove all child nodes of the node with the current position from the game +tree.
+
Keep Only Position
+
Delete all moves and keep only the current position as a setup. This can be +used to create files that start with a given fixed position.
+
Keep Only Subtree
+
Like Keep Only Position but does not delete the moves after the +current position.
+
Setup Mode
+
Enter or leave setup mode. In setup mode, pieces can be placed anywhere on +the board, even in violation of the game rules. Existing pieces can be removed +from the board by clicking on them. The currently selected color also +determines the color to play after the setup is finished. It can be changed +with Next Color or by clicking on the orientation selector while no +piece is selected. Setup mode can only be used if no moves have been played +yet.
+
Next Color
+
Choose the next color for selecting pieces. This can be used for example to +enter game records, in which moves of a color were skipped because the color +ran out of time.
+
+

View

+
+
Toolbar
+
Show or hide the toolbar.
+
Toolbar Text
+
Configure the appearance of the toolbar.
+
Comment
+
Show or hide a text field to display or edit comments on the current +position.
+
Move Marking
+
Change the way moves are marked on the board. The options are to mark the +last move played with a dot or with a number, or to show the numbers of all +moves, or not to show any marks.
+
Coordinates
+
Display coordinates around the board for the fields on the board. The +convention for the coordinates is the same as in the Blokus SGF file format +used by Pentobi.
+
Show Variations
+
Appends a letter to the move number on the board if the move has +variations. If moves are marked with dots instead of numbers, a circle will be +used instead of a dot for moves not in the main variation.
+
Fullscreen
+
Make the main window full screen or leave full screen mode. It is +platform-dependent if the window menu is shown in full screen mode. To leave +full screen mode without using the window menu, press the F11 key.
+
+

Computer

+
+
Computer Colors
+
Select which colors are played by the computer.
+
Play
+
Make the computer play a move for the current color. This can be used to +change the color the computer plays or to resume playing after navigating in +the game tree. If the computer did not already play the current color, it will +play this color (and only this color) from now on.
+
Play Single Move
+
Make the computer play a single move for the current color without changing +the colors played by the computer.
+
Stop
+
Abort the current move generation. You can make the computer continue to +play by selecting Play.
+
Level
+
Change the playing strength of the computer. Higher levels are stronger but +can make the computer take a long time for playing moves on slow computers. The +computer will remember the level last used separately for each game variant and +restore it when the game variant is changed.
+
+

Tools

+
+
Rating
+
Show a dialog window with the rating of the user in the current game +variant.
+
Analyze Game
+
Perform a game analysis.
+
+

Help

+
+
Pentobi Help
+
Show a window to browse the Pentobi user manual.
+
About Pentobi
+
Show an info dialog with information about this version of Pentobi.
+
+

Previous | Next

+ + diff --git a/src/pentobi/help/de/pentobi/become_stronger.html b/src/pentobi/help/de/pentobi/become_stronger.html new file mode 100644 index 0000000..6f50443 --- /dev/null +++ b/src/pentobi/help/de/pentobi/become_stronger.html @@ -0,0 +1,67 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Ein stärkerer Spieler werden

+

Pentobi besitzt Funktionen, die Ihnen helfen können, ein stärkerer +Blokus-Spieler zu werden.

+

Spielanalyse

+

Sie können ein Spiel analysieren, indem Sie Spiel analysieren aus dem +Extras-Menü wählen. Dies lässt den Computer eine Bewertung jeder +Brettstellung der Hauptvariante ausführen. Das Ergebnis wird in einem Fenster +mit einem Diagramm farbiger Punkte dargestellt.

+

+"Spielanalyse-Fenster"

+
Analyse eines Spiels der Spielvariante +Klassisch (2 Spieler).
+

Jeder Punkt repräsentiert eine Spielstellung, in der die Farbe des Punkts am +Zug war. Die Punkte sind horizontal nach Zugnummer angeordnet. Die vertikale +Achse repräsentiert die Wahrscheinlichkeit, dass die Farbe das Spiel gewinnt. +Mausklicks im Diagramm gehen zur jeweiligen Stellung.

+

Die Werte stellen nur Schätzwerte dar und der Computer wird manchmal +Stellungen nicht korrekt bewerten. Aber ein plötzliches Abfallen des Wertes +kann Ihnen dabei helfen, Züge zu finden, die möglicherweise schlecht waren. Sie +können zur Stellung vor dem Zug zurückgehen und versuchen, einen besseren Zug +zu finden oder den Computer fragen, was er gespielt hätte, indem Sie +Einzelnen Zug spielen aus dem Computer-Menü auswählen.

+

Ihre Wertung ermitteln

+

Sie können Ihre Fortschritte verfolgen, indem Sie gewertete Spiele gegen den +Computer spielen. Die Spielergebnisse werden benutzt, um Ihre gegenwärtige +Wertung zu ermitteln. Die Wertung ist eine Zahl, die Ihre Spielstärke +darstellt.

+

Ein gewertetes Spiel wird mit Gewertetes Spiel aus dem +Spiel-Menü oder der Werkzeugleiste gestartet. Wenn Sie in der +gegenwärtigen Spielvariante noch keine gewerteten Spiele gespielt haben, werden +Sie gefragt, eine Anfangswertung zu wählen, wodurch die Anzahl der Spiele +reduziert wird, die nötig ist, um Ihre wirkliche Wertung zu bestimmen. Falls +Sie Anfänger sind, belassen Sie die Anfangswertung auf 1000.

+

Für jedes gewertete Spiel wird der Computer eine Spielstufe für den +Computerspieler gemäß Ihrer gegenwärtigen Wertung wählen. Die Farbe, die Sie +spielen, wird in jedem Spiel zufällig ausgewählt.

+

Während eines gewerteten Spiels sind die meisten Funktionen, die nicht zum +Spielen benötigt werden, deaktiviert: Sie können keine Züge zurücknehmen, im +Spiel navigieren, die Computer-Farben ändern oder die Spielstufe ändern. Um +eine akkurate Wertung zu erhalten, sollten Sie gewertete Spiele immer bis zum +Ende spielen.

+

Nachdem das Spiel beendet ist, wird Ihre Wertung in Abhängigkeit vom +Spielergebnis und der Spielstufe aktualisiert. Für das Spielergebnis zählt nur, +ob Sie gewonnen oder verloren haben, oder ob das Spiel in einem Unentschieden +endete. Die genaue Anzahl der Spielpunkte spielt keine Rolle.

+

+"Wertungsfenster"

+
Fenster mit Wertungsgraph.
+

Sie können Ihre aktuelle Wertung jederzeit mit Wertung aus dem +Extras-Menü sehen. Dies öffnet ein Fenster, in dem die Entwicklung Ihrer +Wertung während der letzten 100 Spiele als Graph gezeigt wird. Die letzten 100 +Spiele werden automatisch gespeichert und können durch Doppelklick auf die +Zeilen der Spieltabelle unter dem Graph geladen werden.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/callisto_rules.html b/src/pentobi/help/de/pentobi/callisto_rules.html new file mode 100644 index 0000000..8c53e36 --- /dev/null +++ b/src/pentobi/help/de/pentobi/callisto_rules.html @@ -0,0 +1,50 @@ + + + +Pentobi Help + + + + +

Zurück | Weiter

+

Callisto-Regeln

+

Callisto ist ein weiteres Brettspiel ähnlich wie Blokus. Das Spielbrett ist +vom klassischen 20×20-Blokus-Spielbrett abgeleitet, indem die Ecken entfernt +werden, sodass ein Achteck verbleibt mit einer oberen Kantenlänge von sechs. +Die Spielsteine sind eine Untermenge der Polyominos bis zur Größe fünf. Sie +beinhalten drei 1×1-Spielsteine pro Spieler, die eine besondere Rolle +spielen.

+

+"Spielsteine

+
Die 21 Spielsteine.
+

Die 1×1-Spielsteine dürfen überall auf dem Spielbrett gesetzt werden außer +im Zentrum des Spielbretts. Das Zentrums besteht aus einem Achteck mit Breite +sechs und oberer Kantenlänge zwei. Die ersten zwei Züge eines Spielers müssen +einen 1×1-Spielstein benutzen, der dritte 1×1-Spielstein kann jederzeit später +gespielt werden.

+

+"Spielbrett

+
Das Brett mit einer dunkleren Farbe im +Zentrum.
+

Alle größeren Spielsteine dürfen überall auf dem Brett gesetzt werden, +müssen aber einen existierenden Spielstein der selben Farbe Kante an Kante +berühren.

+

+"Beispielstellung

+
Eine Beispielstellung nach ein paar +Zügen.
+

Die Punktzahl einer Farbe ist die Anzahl der Quadrate auf dem Brett, die von +der Farbe bedeckt sind, wobei die 1×1-Spielsteine nicht gezählt werden. +Bonuspunkte werden nicht verwendet. Anders als in Blokus werden Unentschieden +zugunsten des Spielers aufgelöst, der später begonnen hat.

+

Regeln für zwei oder drei Spieler

+

Das Spiel kann mit weniger als vier Spielern gespielt werden, indem ein +kleineres Spielbrett verwendet wird. Für drei Spieler ist das Brett ein Achteck +mit Breite 20 und obererer Kantenlänge zwei. Für zwei Spieler ist das Brett ein +Achteck mit Breite 16 und obererer Kantenlänge zwei. Die Größe des Zentrums +bleibt gleich.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/classic_rules.html b/src/pentobi/help/de/pentobi/classic_rules.html new file mode 100644 index 0000000..fefcf6d --- /dev/null +++ b/src/pentobi/help/de/pentobi/classic_rules.html @@ -0,0 +1,65 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Klassische Regeln

+

Es gibt vier Spieler, Blau, Gelb, Rot und Grün, und ein Brett, das aus 20×20 +Quadraten besteht.

+

Jeder Spieler besitzt 21 Spielsteine seiner Farbe, die die Form von +Polyominos bis zur Größe fünf haben (ein Polyomino ist eine Figur, die aus +einer Anzahl von Quadraten besteht, die entlang der Kanten verbunden sind).

+

+"Spielsteine

+
Die 21 Spielsteine.
+

Die Spieler setzen abwechselnd einen ihrer Spielsteine aufs Brett. Blau +fängt an, gefolgt von Gelb, dann Rot, dann Grün.

+

Jeder Spieler hat ein Startfeld. Das Startfeld von Blau ist in der oberen +linken Ecke, das von Gelb in der oberen rechten Ecke, das von Rot in der +unteren rechten Ecke und das von Grün in der unteren linken Ecke. Der erste +Spielstein eines Spielers muss sein Startfeld abdecken.

+

+"Spielbrett

+
Das 20×20-Brett mit den Startfeldern
+durch farbige Punkte markiert.
+

Die folgenden Spielsteine müssen so auf leere Quadrate gesetzt werden, dass +der neue Spielstein mindestens einen Spielstein der eigenen Farbe Ecke an Ecke +berührt, aber keinen Spielstein der eigenen Farbe entlang der Kanten. Der neue +Spielstein darf die Kanten von gegnerischen Spielsteinen berühren.

+

+"Beispielstellung

+
Eine Beispielstellung nach ein paar +Zügen.
+

Wenn der Spieler einer Farbe keine Spielsteine mehr setzen kann, muss der +Spieler aussetzen und die nächste Farbe ist am Zug.

+

Wenn keiner der Spieler mehr einen Spielstein setzen kann, gewinnt der +Spieler mit der höchsten Punktzahl. Die Punktzahl einer Farbe ist die Anzahl +der Quadrate auf dem Brett, die von der Farbe besetzt sind, plus ein Bonus von +15 Punkten, wenn die Farbe alle ihre Spielsteine setzen konnte, plus ein +zusätzlicher Bonus von 5 Punkten, wenn die Farbe alle Spielsteine setzen konnte +und der zuletzt gespielte Spielstein der Spielstein war, der aus einem Quadrat +besteht.

+

Regeln für zwei Spieler

+

Das Spiel kann mit zwei Spielern gespielt werden. Der erste Spieler spielt +Blau und Rot, der zweite Spieler Gelb und Grün. Die Punkte von beiden Farben +eines Spielers werden addiert.

+

Regeln für drei Spieler

+

Das Spiel kann auch mit drei Spielern gespielt werden. Die Spieler wechseln +sich beim Spielen der vierten Farbe (Grün) ab. Am Spielende wird die Punktzahl +von Grün ignoriert.

+

Farblose Startfelder

+

Beachten Sie, dass die ursprünglichen klassischen Regeln für Blokus farblose +Startfelder benutzen. Dies bedeutet, dass jede Farbe frei wählen darf, welches +der verbleibenden noch freien Startfelder sie für ihren ersten Zug benutzt. +Pentobi unterstützt zur Zeit nur die Regelvariante mit farbigen Startfeldern, +weil diese Variante auf dem Blokus-Online-Server auf blokus.com und in den +meisten bisherigen Blokus-Turnieren verwendet wurde.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/duo_rules.html b/src/pentobi/help/de/pentobi/duo_rules.html new file mode 100644 index 0000000..bdc1318 --- /dev/null +++ b/src/pentobi/help/de/pentobi/duo_rules.html @@ -0,0 +1,29 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Duo-Regeln

+

Die Spielvariante Duo ist eine andere Spielvariante für zwei Spieler. Das +Spiel wird auf einem kleineren Brett mit 14×14 Quadraten gespielt. Es gibt eine +Farbe pro Spieler (Blau und Grün) und die Startfelder befinden sich nicht in +den Ecken, sondern auf dem Feld mit den Koordinaten (5,10) für Blau und auf +(10,5) für Grün.

+

+"Spielbrett

+
Das 14×14-Brett, das in der Spielvariante +Duo benutzt
+wird, mit den Startfeldern durch farbige Punkte markiert.
+

+"Beispielstellung

+
Eine Beispielstellung in der Spielvariante +Duo.
+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/index.html b/src/pentobi/help/de/pentobi/index.html new file mode 100644 index 0000000..6d22c49 --- /dev/null +++ b/src/pentobi/help/de/pentobi/index.html @@ -0,0 +1,29 @@ + + + +Pentobi-Hilfe + + + + +

Weiter

+

Pentobi

+

Pentobi ist ein Computer-Gegner für das Brettspiel Blokus. In diesem Spiel +setzen vier Spieler Spielsteine, die ähnlich den Spielsteinen des +Computerspiels Tetris sind, auf ein 20×20-Brett. Pentobi unterstützt auch die +Spielvarianten für zwei oder drei Spieler und die Spielvarianten Duo, Trigon, +Junior, Nexos und Callisto.

+

Klassische Regeln
+Duo-Regeln
+Trigon-Regeln
+Junior-Regeln
+Nexos-Regeln
+Callisto-Regeln
+Wie Sie Pentobi benutzen
+Ein stärkerer Spieler werden
+Das Fenstermenü
+Tastenkürzel
+Systemvoraussetzungen
+Lizenz

+ + diff --git a/src/pentobi/help/de/pentobi/junior_rules.html b/src/pentobi/help/de/pentobi/junior_rules.html new file mode 100644 index 0000000..15f9ba9 --- /dev/null +++ b/src/pentobi/help/de/pentobi/junior_rules.html @@ -0,0 +1,24 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Junior-Regeln

+

Junior ist eine vereinfachte Spielvariante für zwei Spieler. Es wird auf dem +gleichen 14×14-Brett gespielt wie die Spielvariante Duo, benutzt aber nur eine +Teilmenge der Pentominos und die Spieler bekommen zwei von jedem dieser +Pentominos.

+

+"Spielsteine

+
Die 24 Spielsteine, die in Junior benutzt +werden.
+

Bonuspunkte werden in Junior nicht benutzt.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/license.html b/src/pentobi/help/de/pentobi/license.html new file mode 100644 index 0000000..e4e2b74 --- /dev/null +++ b/src/pentobi/help/de/pentobi/license.html @@ -0,0 +1,26 @@ + + + +Pentobi-Hilfe + + + + +

Zurück

+

Lizenz

+

Copyright © 2011–2017 Markus Enzenberger

+

Dieses Programm ist freie Software. Sie können es unter den Bedingungen der +GNU General Public License, wie von der Free Software Foundation +veröffentlicht, weitergeben und/oder modifizieren, entweder gemäß Version 3 der +Lizenz oder (nach Ihrer Wahl) jeder späteren Version.

+

Die Veröffentlichung dieses Programms erfolgt in der Hoffnung, dass es Ihnen +von Nutzen sein wird, aber OHNE IRGENDEINE GARANTIE, insbesondere ohne eine +implizite Garantie der MARKTREIFE oder der VERWENDBARKEIT FÜR EINEN BESTIMMTEN +ZWECK. Nähere Angaben finden Sie in der GNU General Public License.

+

Hinweis zu Markennamen

+

Der Markenname Blokus und andere erwähnte Marken sind Eigentum ihrer +jeweiligen Markeninhaber. Die Markeninhaber stehen in keiner Verbindung mit dem +Autor des Programms Pentobi.

+

Zurück

+ + diff --git a/src/pentobi/help/de/pentobi/nexos_rules.html b/src/pentobi/help/de/pentobi/nexos_rules.html new file mode 100644 index 0000000..84d0e1d --- /dev/null +++ b/src/pentobi/help/de/pentobi/nexos_rules.html @@ -0,0 +1,47 @@ + + + +Pentobi Help + + + + +

Zurück | Weiter

+

Nexos-Regeln

+

Nexos ist ein Brettspiel ähnlich wie Blokus. Das Spielbrett ist ein +rechtwinkliges 13×13-Liniengitter. Jede Farbe benutzt 24 Spielsteine, die aus +bis zu vier verbundenen Liniensegmenten bestehen.

+

+"Spielsteine

+
Die 24 Spielsteine.
+

Jede Farbe hat einen Startkreuzungspunkt auf der Kreuzung der dritten Linien +nahe einer Ecke. Der erste Spielstein muss den Startkreuzungspunkt +berühren.

+

+"Spielbrett

+
Das Brett für Nexos mit den die +Startkreuzungspunkte
+berührenden Segmenten durch farbige Punkte markiert.
+

Die folgenden Spielsteine müssen so auf leere Liniensegmente gesetzt werden, +dass ein Segment des neuen Spielsteins einen Kreuzungspunkt berührt, den +bereits ein Segment derselben Farbe berührt. Es spielt keine Rolle, ob +Spielsteine anderer Farbe denselben Kreuzungspunkt berühren oder bedecken. +Allerdings dürfen sich Spielsteine nicht überlappen. Die Verbindungen zwischen +den Segmenten innerhalb eines Spielsteins sind so, dass zwei rechtwinklige +Verbindungen verschiedener Spielsteine denselben Kreuzungspunkt bedecken können +ohne sich zu überlappen, während gerade Verbindungen das nicht können.

+

+"Beispielstellung

+
Eine Beispielstellung nach ein paar +Zügen.
+

Die Punktzahl einer Farbe ist die Anzahl der Liniensegmente auf dem Brett, +die von der Farbe bedeckt sind, plus ein Bonus von 10 Punkten, wenn die Farbe +alle ihre Spielsteine setzen konnte.

+

Regeln für zwei Spieler

+

Wie Blokus kann Nexos von zwei Spielern gespielt werden, indem ein Spieler +Rot und Blau, und der andere Spieler Gelb und Grün spielt.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/shortcuts.html b/src/pentobi/help/de/pentobi/shortcuts.html new file mode 100644 index 0000000..3805109 --- /dev/null +++ b/src/pentobi/help/de/pentobi/shortcuts.html @@ -0,0 +1,59 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Tastenkürzel

+

Zusätzlich zu den Tastenkürzeln der Menüpunkte, die im Fenstermenü gezeigt +werden, werden die folgenden weiteren Tastenkürzel von Pentobi unterstützt. +Beachten Sie, dass diese Tastenkürzel nicht aktiv sind, wenn das Kommentarfeld +sichtbar ist und den Fokus besitzt. In diesem Fall kann der Fokus vom +Kommentartext durch die Tabulator-Taste entfernt werden.

+
+
Plus
+
+

Nächsten Spielstein auswählen

+
+
Minus
+
+

Vorherigen Spielstein auswählen

+
+
0
+
+

Spielsteinauswahl löschen

+
+
Leertaste
+
+

Nächste Ausrichtung des ausgewählten Spielsteins

+
+
Umschalt+Leertaste
+
+

Vorherige Ausrichtung des ausgewählten Spielsteins

+
+
Links, Rechts, Oben, Unten
+
+

Bewegen des ausgewählten Spielsteins.

+
+
Enter
+
+

Spielen des ausgewählten Spielsteins.

+
+
1, 2, A, C, E, F, G, H, I, J, L, N, O, P, S, T, U, V, W, X, Y, Z
+
+

Einen Spielstein entsprechend den üblicherweise benutzten Spielsteinnamen +auswählen. Wenn es mehrere Spielsteine mit dem Buchstaben gibt (z. B. I3, +I4, I5), dann kann durch mehrmaliges Drücken der Taste zwischen ihnen +gewechselt werden. Einige Buchstaben werden nur in bestimmten Spielvarianten +benutzt. Zum Beispiel wird A nur in Trigon für die Spielsteine A6 und A4 +benutzt (auch bekannt als „Hummer“ und „Dreieck“).

+
+
+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/system.html b/src/pentobi/help/de/pentobi/system.html new file mode 100644 index 0000000..26ed70c --- /dev/null +++ b/src/pentobi/help/de/pentobi/system.html @@ -0,0 +1,22 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Systemvoraussetzungen

+

Minimum: 1 GB RAM, 1 GHz CPU
+Empfohlen für Spielstufe 9: 4 GB RAM, 2 GHz Dual-Core- oder schnellere +CPU

+

Pentobi funktioniert auch auf Systemen, die das Systemminimum nicht +erfüllen, aber die höchste Spielstufe kann auf diesen Systemen sehr langsam +sein (wenn die CPU zu langsam ist) oder eine reduzierte Spielstärke haben (wenn +nicht genügend Arbeitsspeicher vorhanden ist).

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/trigon_rules.html b/src/pentobi/help/de/pentobi/trigon_rules.html new file mode 100644 index 0000000..d4b99cf --- /dev/null +++ b/src/pentobi/help/de/pentobi/trigon_rules.html @@ -0,0 +1,47 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Trigon-Regeln

+

Trigon ist eine weitere Spielvariante. Die Regeln sind ähnlich wie in der +Spielvariante Klassisch, aber es werden ein anders geformtes Brett und andere +Spielsteine verwendet. Jede Farbe benutzt 22 Spielsteine, die wie die +Polyiamonds bis zur Größe sechs geformt sind (ein Polyiamond ist eine Figur, +die aus einer Anzahl von gleichseitigen Dreiecken besteht, die entlang der +Kanten verbunden sind).

+

+"Spielsteine

+
Die 22 Trigon-Spielsteine.
+

Das Spielbrett besteht ebenfalls aus Dreiecken und hat die Form eines +Sechsecks mit jeweils neun Dreiecken pro Kante.

+

+"Spielbrett

+
Das Brett mit den Startfeldern
+durch graue Punkte markiert.
+

Es gibt sechs Startfelder auf dem Brett, jedes in der Mitte der vierten +Reihe von jeder Kante aus gesehen. Die Startfelder sind nicht farbig und die +Spieler dürfen das Startfeld für den ersten Spielstein einer Farbe frei +wählen.

+

+"Beispielstellung

+
Eine Beispielstellung nach ein paar +Zügen.
+

Regeln für zwei Spieler

+

Wie die Spielvariante Klassisch kann Trigon mit zwei Spielern gespielt +werden, indem ein Spieler Blau und Rot und der andere Gelb und Grün spielt.

+

Regeln für drei Spieler

+

Trigon kann mit drei Spielern gespielt werden, wobei dieselben Regeln wie +für die Variante mit vier Spielern benutzt werden. Die Variante für drei +Spieler wird auf einem kleineren Spielbrett mit einer Kantenlänge von acht +Dreiecken gespielt. Die Startfelder sind in der Mitte der dritten Reihe von +jeder Kante aus gesehen.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/user_interface.html b/src/pentobi/help/de/pentobi/user_interface.html new file mode 100644 index 0000000..6a5dc95 --- /dev/null +++ b/src/pentobi/help/de/pentobi/user_interface.html @@ -0,0 +1,80 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Wie Sie Pentobi benutzen

+

Spielbrett

+

Pentobis Hauptfenster zeigt das Spielbrett auf der linken Seite. Auf den +gespielten Spielsteinen auf dem Brett können sich Nummern befinden, die die +Zugnummer angeben, zu der der Spielstein gespielt wurde. Ein Buchstabe nach der +Zugnummer zeigt an, dass zu diesem Zug eine Variante existiert (siehe +unten).

+

Spielsteine können gespielt werden, indem sie mit der Maus oder den +Pfeiltasten an eine Position gebracht werden, die einem legalen Zug entspricht, +und dann die linke Maustaste oder die Eingabetaste gedrückt wird.

+

Spielsteine und Punkte

+

Auf der rechten Seite werden die verbleibenden Spielsteine gezeigt. Über den +verbleibenden Spielsteinen befinden sich eine Orientierungsauswahl, die den +ausgewählten Spielstein zeigt und es dem Spieler erlaubt, seine Orientierung zu +ändern. Wenn kein Spielstein ausgewählt ist und das Spiel noch nicht beendet +ist, zeigt ein farbiger Punkt in der Orientierungsauswahl, welche Farbe am Zug +ist.

+

Spielsteine können durch Klicken auf einen gezeigten verbleibenden +Spielstein ausgewählt werden, durch Benutzen der Buttons mit dem +Links/Rechts-Pfeil in der Orientierungsauswahl oder durch Benutzen von Tastenkürzeln.

+

Unterhalb der Orientierungsauswahl befindet sich eine Punkteanzeige, die die +gegenwärtigen Punkte für jede Farbe oder jeden Spieler zeigt. Die Punkte sind +die Summe aus den Punkten auf dem Spielbrett und den Bonuspunkten. Punkte sind +unterstrichen, wenn sie endgültig sind, weil die Farbe keine Spielsteine mehr +spielen kann. Eine kleiner Stern zeigt an, dass die Punkte einen Bonus +beinhalten.

+

Gegen den Computer spielen

+

Das Spielbrett kann benutzt werden, um Partien einzugeben, die von Menschen +gespielt werden, oder um Spiele gegen den Computer zu spielen. In Spielen gegen +den Computer kann der Computer jede der Farben (oder mehrere) spielen.

+

Wenn Sie ein neues Spiel beginnen, ist voreingestellt, dass der Mensch die +Farbe(n) des ersten Spielers spielt und der Computer alle anderen Farben. Um +dies zu ändern, benutzen Sie Computer-Farben aus dem Menü +Computer oder der Werkzeugleiste und wählen Sie die Farben, die der +Computer spielen soll.

+

Die Ausnahme ist, dass es voreingestellt ist, dass der Computer keine Farbe +spielt, wenn er im letzten Spiel keine Farbe gespielt hat. Damit wird +vermieden, dass der Computer unbeabsichtigt automatisch zu spielen beginnt, +wenn der Benutzer das Spielbrett hauptsächlich zum Eingeben von Zugsequenzen +oder ähnliche Aufgaben benutzen will. Wenn Sie also das Spielbrett benutzen +wollen ohne gegen den Computer zu spielen, brauchen Sie nur einmal die Farben +des Computers im Dialogfenster Computer-Farben abschalten und diese +Einstellung wird sich nicht ändern. Nach dem Laden eines Spiels ist ebenfalls +voreingestellt, dass der Computer keine Farbe spielt.

+

Die Auswahl von Spielen aus dem Menü Computer oder der +Werkzeugleiste lässt den Computer immer einen Zug für die gegenwärtige Farbe +spielen. Wenn der Computer diese Farbe bisher nicht gespielt hat, wird er +außerdem im weiteren Spielverlauf diese Farbe (und nur diese Farbe) +spielen.

+

Zugvarianten und der Spielbaum

+

Wenn Sie ein Spiel spielen, wird Pentobi die Abfolge der Züge speichern und +es ist jederzeit möglich, zu einer früheren Brettstellung zurückzugehen und +anders zu spielen. Wenn Sie das tun, wird die neue Zugfolgen als eine +alternative Zugfolge (genannt Variante) gespeichert. Varianten können auch von +Kommentatoren benutzt werden, um Kommentierungen zu existierenden Spielen +hinzuzufügen. Varianten können in jeder Brettstellung existieren und ihrerseits +Untervarianten besitzen. Das Spiel kann daher zu einem Spielbaum werden, in dem +jeder Knoten eine Brettstellung repräsentiert. Sie können im Spielbaum mit den +Menüpunkten des Menüs Gehe zu oder der Werkzeugleiste navigieren.

+

Die Hauptvariante ist die Zugfolge, die in der Startstellung beginnt und +immer den ersten Kindknoten in jeder Brettstellung wählt (z. B. indem Sie +Vorwärts im Menü Gehe zu oder der Werkzeugleiste benutzen). Die +Hauptvariante sollte das wirklich gespielte Spiel darstellen. Wenn Sie eine +Nebenvariante zur Hauptvariante machen wollen, wählen Sie Zu Hauptvariante +machen aus dem Bearbeiten-Menü.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/window_menu.html b/src/pentobi/help/de/pentobi/window_menu.html new file mode 100644 index 0000000..e861dbe --- /dev/null +++ b/src/pentobi/help/de/pentobi/window_menu.html @@ -0,0 +1,226 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Das Fenstermenü

+

Spiel

+
+
Neu
+
Beginnt ein neues Spiel.
+
Gewertetes Spiel
+
Beginnt ein neues gewertetes +Spiel gegen den Computer.
+
Spielvariante
+
Wählt eine Spielvariante und beginnt ein neues Spiel dieser +Spielvariante.
+
Spielinformation
+
Öffnet ein Dialogfenster zum Anzeigen oder Bearbeiten zusätzlicher +Informationen über das Spiel, wie die Namen der Spieler oder das Datum, an dem +das Spiel gespielt wurde.
+
Zug rückgängig
+
Nimmt den zuletzt gespielten Zug zurück und entfernt ihn aus dem Spielbaum. +Das Zurücknehmen eines Zugs ist nur möglich, wenn er der letzte Zug der +gegenwärtigen Variante ist (d. h. ein Endknoten im Spielbaum; benutzen Sie +Bearbeiten/Abschneiden zum Entfernen innerer Knoten aus dem +Spielbaum).
+
Zug finden
+
Findet einen legalen Zug für die gegenwärtige Farbe und zeigt ihn für ein +paar Sekunden auf dem Spielbrett. Das wiederholte Auswählen dieses Menüpunkts +zeigt alle legalen Züge.
+
Öffnen
+
Lädt ein gespeichertes Spiel. Die Brettstellung nach dem Laden ist die +letzte Stellung in der Hauptvariante, sofern das Spiel nicht mit einer +aufgebauten Brettstellung beginnt. Wenn das Spiel mit einer aufgebauten +Brettstellung beginnt, ist die Stellung nach dem Laden stattdessen die +Anfangsstellung. Dies vermeidet, dass Lösungen sofort angezeigt werden, wenn +die Datei ein Blokus-Problem als aufgebaute Brettstellung enthält mit der +Lösung in der Hauptvariante.
+
Zuletzt benutzte Dateien
+
Lädt ein kürzlich benutztes Spiel.
+
Speichern
+
Speichert das gegenwärtige Spiel.
+
Speichern unter
+
Speichert das gegenwärtige Spiel unter einem neuen Dateinamen.
+
Exportieren/Grafik
+
Speichert die gegenwärtige Brettstellung als eine Grafikdatei. Mehrere +Grafikdateiformate werden unterstützt, das Dateiformat wird von der Dateiendung +abgeleitet (z. B. „.png“ für das PNG-Format).
+
Exportieren/ASCII-Art
+
Speichert die gegenwärtige Brettstellung als Textdiagramm. Das Textdiagramm +sollte mit einer Schriftart fester Breite betrachtet werden.
+
Beenden
+
Beendet Pentobi.
+
+

Gehe zu

+
+
Anfang
+
Geht zum Anfang des Spiels.
+
Zurück
+
Geht einen Zug in der gegenwärtigen Variante zurück. Der entsprechende +Button in der Werkzeugleiste unterstützt automatische Wiederholung, wenn er +gedrückt gehalten wird.
+
Vorwärts
+
Geht einen Zug in der gegenwärtigen Variante vorwärts. Wenn die +gegenwärtige Brettstellung mehrerer nachfolgende Varianten hat (d. h. der +gegenwärtige Knoten im Spielbaum mehrere Kindknoten hat), wird die erste +Variante benutzt. Der entsprechende Button in der Werkzeugleiste unterstützt +automatische Wiederholung, wenn er gedrückt gehalten wird.
+
Ende
+
Geht zum Ende der gegenwärtigen Variante. Wie bei Vorwärts wird auch +hier jeweils die erste Variante benutzt, wenn die Brettstellung mehrere +nachfolgende Varianten hat.
+
Nächste Variante
+
Geht zur nächsten Variante zum zuletzt gespielten Zug (d. h. zum +nächsten Geschwisterknoten des gegenwärtigen Knotens im Spielbaum).
+
Vorherige Variante
+
Geht zur vorherigen Variante zum zuletzt gespielten Zug (d. h. zum +vorherigen Geschwisterknoten des gegenwärtigen Knotens im Spielbaum).
+
Gehe zu Zug
+
Geht zum Zug mit einer bestimmten Nummer in der gegenwärtigen +Variante.
+
Zurück zu Hauptvariante
+
Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die +zur Hauptvariante gehörte.
+
Anfang der Verzweigung
+
Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die +einen alternativen Zug hatte.
+
Nächsten Kommentar finden
+
Geht zur nächsten Brettstellung, die einen Kommentar besitzt. Wenn das +Kommentarfeld nicht sichtbar ist, wird es sichtbar gemacht. Das wiederholte +Auswählen dieses Menüpunkts zeigt nacheinander alle Brettstellungen mit +Kommentaren im Spielbaum.
+
+

Bearbeiten

+
+
Zugkommentierung
+
Fügt ein wie in der Schachnotation benutztes Symbol (z. B. !!) zum +gegenwärtigen Zug hinzu. Die Symbole werden an die Zugnummern in der +Statusleiste angehängt und, abhängig von der Einstellung von +Zugmarkierung, an die auf dem Spielbrett.
+
Zu Hauptvariante machen
+
Macht die gegenwärtige Variante zur Hauptvariante des Spiels. Dies ordnet +die Knoten im Spielbaum so um, dass die gegenwärtige Variante zur Hauptvariante +wird.
+
Variante nach oben schieben
+
Ändert die Reihenfolge der Varianten so, dass die gegenwärtige +Brettstellung beim Durchlaufen der Varianten mit Nächste/Vorherige +Variante früher erscheint.
+
Variante nach unten schieben
+
Ändert die Reihenfolge der Varianten so, dass die gegenwärtige +Brettstellung beim Durchlaufen der Varianten mit Nächste/Vorherige +Variante später erscheint.
+
Alle Varianten löschen
+
Löscht alle Varianten außer der Hauptvariante. Wenn sich die gegenwärtige +Brettstellung nicht in der Hauptvariante befindet, wird zuvor zu einer +Brettstellung in der Hauptvariante gewechselt wie in Zurück zu +Hauptvariante.
+
Abschneiden
+
Entfernt den Knoten mit der gegenwärtigen Brettstellung zusammen mit dem +auf ihn folgenden Teilbaum aus dem Spielbaum.
+
Kindknoten abschneiden
+
Entfernt alle Kindknoten des Knotens mit der gegenwärtigen Brettstellung +aus dem Spielbaum.
+
Nur Brettstellung behalten
+
Löscht alle Züge und behält nur die gegenwärtige Brettstellung als feste +Stellung. Dies kann zur Erzeugung von Dateien benutzt werden, die mit einer +festgelegten Brettstellung beginnen.
+
Nur Teilbaum behalten
+
Wie Nur Brettstellung behalten, aber die Züge nach der gegenwärtigen +Brettstellung werden nicht gelöscht.
+
Stellungsaufbau
+
Aktiviert oder deaktiviert den Stellungsaufbau-Modus. Im +Stellungsaufbau-Modus können Spielsteine überall auf dem Brett abgelegt werden, +auch unter Verletzung der Spielregeln. Existierende Spielsteine können durch +Anklicken vom Brett entfernt werden. Die gegenwärtig gewählte Farbe legt auch +die Farbe fest, die nach Beenden des Stellungsaufbaus am Zug ist. Sie kann mit +Nächste Farbe oder durch Klicken auf die Orientierungsauswahl während +kein Spielstein ausgewählt ist geändert werden. Der Stellungsaufbau-Modus kann +nur benutzt werden, wenn noch keine Züge gespielt wurden.
+
Nächste Farbe
+
Wählt die nächste Farbe zum Auswählen eines Spielsteins. Dies kann zum +Beispiel benutzt werden, um Partien einzugeben, bei denen Züge einer Farbe +übersprungen wurden, da die Farbe aufgrund einer Bedenkzeitüberschreitung vom +Weiterspielen ausgeschlossen wurde.
+
+

Ansicht

+
+
Werkzeugleiste
+
Zeigt oder verbirgt die Werkzeugleiste.
+
Werkzeugleistentext
+
Konfiguriert das Aussehen der Werkzeugleiste.
+
Kommentar
+
Zeigt oder verbirgt ein Textfeld zum Anzeigen oder Bearbeiten von +Kommentaren zur gegenwärtigen Brettstellung.
+
Zugnummern
+
Ändert die Anzeigeart von Zugnummern auf dem Spielbrett. Die Optionen sind +nur die Nummer des zuletzt gespielten Zugs zu zeigen (oder der zuletzt +gespielten Züge, wenn der Computer mehrere Züge nacheinander spielt) oder die +Nummern aller Züge zu zeigen oder gar keine Nummern zu zeigen.
+
Zugmarkierung
+
Ändert die Markierung von Zügen auf dem Spielbrett. Die Optionen sind, den +zuletzt gespielten Zug mit einem Punkt oder einer Nummer zu markieren, oder die +Nummern aller Züge zu zeigen oder gar keine Markierung zu zeigen.
+
Koordinaten
+
Zeigt Koordinaten an den Rändern des Spielbretts für die Felder auf dem +Spielbrett. Die Konvention für die Koordinaten ist dieselbe wie im von Pentobi +benutzten Blokus-SGF-Dateiformat.
+
Varianten zeigen
+
Fügt einen Buchstaben an die Zugnummer auf dem Spielbrett an, wenn der Zug +Varianten besitzt. Wenn Züge mit einem Punkt statt einer Nummer markiert +werden, wird ein Kreis statt ein Punkt für Züge verwendet, die nicht in der +Hauptvariante sind.
+
Vollbild
+
Schaltet das Hauptfenster in den Vollbildmodus oder verlässt den +Vollbildmodus. Es ist systemabhängig, ob das Fenstermenü im Vollbildmodus +angezeigt wird. Um den Vollbildmodus ohne das Benutzen des Fenstermenüs zu +verlassen, drücken Sie die F11-Taste.
+
+

Computer

+
+
Computer-Farben
+
Wählt aus, welche Farben vom Computer gespielt werden.
+
Spielen
+
Lässt den Computer einen Zug für die gegenwärtige Farbe spielen. Dies kann +zum Ändern der Computer-Farbe benutzt werden oder um nach dem Navigieren im +Spielbaum mit dem Spielen fortzufahren. Wenn der Computer die gegenwärtige +Farbe nicht bereits spielte, wird er diese Farbe (und nur diese) im weiteren +Spielverlauf spielen.
+
Einzelnen Zug spielen
+
Lässt den Computer einen einzelnen Zug für die gegenwärtige Farbe spielen +ohne die vom Computer gespielten Farben zu ändern.
+
Stopp
+
Bricht die gegenwärtige Zuggenerierung ab. Sie können den Computer +weiterspielen lassen, indem Sie Spielen auswählen.
+
Spielstufe
+
Ändert die Spielstärke des Computers. Höhere Spielstufen sind stärker, +können aber die Bedenkzeiten des Computers auf langsamen Computern sehr +verlängern.
+
+

Extras

+
+
Wertung
+
Zeigt ein Dialogfenster mit der Wertung des Benutzers in der gegenwärtigen +Spielvariante.
+
Spiel analysieren
+
Führt eine Spielanalyse +durch.
+
+

Hilfe

+
+
Pentobi-Hilfe
+
Zeigt ein Fenster mit dem Pentobi-Benutzerhandbuch.
+
Über Pentobi
+
Zeigt eine Dialogfenster mit Informationen über diese Version von +Pentobi.
+
+

Zurück | Weiter

+ + diff --git a/src/pentobi/icons.qrc b/src/pentobi/icons.qrc new file mode 100644 index 0000000..735f5c8 --- /dev/null +++ b/src/pentobi/icons.qrc @@ -0,0 +1,15 @@ + + + + icons/pentobi-backward.svg + icons/pentobi-beginning.svg + icons/pentobi-computer-colors.svg + icons/pentobi-end.svg + icons/pentobi-forward.svg + icons/pentobi-newgame.svg + icons/pentobi-next-variation.svg + icons/pentobi-play.svg + icons/pentobi-previous-variation.svg + icons/pentobi-undo.svg + + diff --git a/src/pentobi/icons/pentobi-16.svg b/src/pentobi/icons/pentobi-16.svg new file mode 100644 index 0000000..9688842 --- /dev/null +++ b/src/pentobi/icons/pentobi-16.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-32.svg b/src/pentobi/icons/pentobi-32.svg new file mode 100644 index 0000000..387a5b4 --- /dev/null +++ b/src/pentobi/icons/pentobi-32.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-64.svg b/src/pentobi/icons/pentobi-64.svg new file mode 100644 index 0000000..4f371cd --- /dev/null +++ b/src/pentobi/icons/pentobi-64.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-backward-16.svg b/src/pentobi/icons/pentobi-backward-16.svg new file mode 100644 index 0000000..42a4540 --- /dev/null +++ b/src/pentobi/icons/pentobi-backward-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-backward.svg b/src/pentobi/icons/pentobi-backward.svg new file mode 100644 index 0000000..7fbe3eb --- /dev/null +++ b/src/pentobi/icons/pentobi-backward.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-beginning-16.svg b/src/pentobi/icons/pentobi-beginning-16.svg new file mode 100644 index 0000000..68f4afb --- /dev/null +++ b/src/pentobi/icons/pentobi-beginning-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-beginning.svg b/src/pentobi/icons/pentobi-beginning.svg new file mode 100644 index 0000000..9e754ac --- /dev/null +++ b/src/pentobi/icons/pentobi-beginning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-computer-colors-16.svg b/src/pentobi/icons/pentobi-computer-colors-16.svg new file mode 100644 index 0000000..aa6f6ac --- /dev/null +++ b/src/pentobi/icons/pentobi-computer-colors-16.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-computer-colors.svg b/src/pentobi/icons/pentobi-computer-colors.svg new file mode 100644 index 0000000..c9870f0 --- /dev/null +++ b/src/pentobi/icons/pentobi-computer-colors.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-end-16.svg b/src/pentobi/icons/pentobi-end-16.svg new file mode 100644 index 0000000..d249048 --- /dev/null +++ b/src/pentobi/icons/pentobi-end-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-end.svg b/src/pentobi/icons/pentobi-end.svg new file mode 100644 index 0000000..f657d97 --- /dev/null +++ b/src/pentobi/icons/pentobi-end.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-flip-horizontal.svg b/src/pentobi/icons/pentobi-flip-horizontal.svg new file mode 100644 index 0000000..193ba08 --- /dev/null +++ b/src/pentobi/icons/pentobi-flip-horizontal.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/icons/pentobi-flip-vertical.svg b/src/pentobi/icons/pentobi-flip-vertical.svg new file mode 100644 index 0000000..1d266c3 --- /dev/null +++ b/src/pentobi/icons/pentobi-flip-vertical.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/icons/pentobi-forward-16.svg b/src/pentobi/icons/pentobi-forward-16.svg new file mode 100644 index 0000000..ebce7fd --- /dev/null +++ b/src/pentobi/icons/pentobi-forward-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-forward.svg b/src/pentobi/icons/pentobi-forward.svg new file mode 100644 index 0000000..8957c14 --- /dev/null +++ b/src/pentobi/icons/pentobi-forward.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-newgame-16.svg b/src/pentobi/icons/pentobi-newgame-16.svg new file mode 100644 index 0000000..17df58f --- /dev/null +++ b/src/pentobi/icons/pentobi-newgame-16.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-newgame.svg b/src/pentobi/icons/pentobi-newgame.svg new file mode 100644 index 0000000..1a2c884 --- /dev/null +++ b/src/pentobi/icons/pentobi-newgame.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-next-piece.svg b/src/pentobi/icons/pentobi-next-piece.svg new file mode 100644 index 0000000..f3d844d --- /dev/null +++ b/src/pentobi/icons/pentobi-next-piece.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/icons/pentobi-next-variation-16.svg b/src/pentobi/icons/pentobi-next-variation-16.svg new file mode 100644 index 0000000..72a9727 --- /dev/null +++ b/src/pentobi/icons/pentobi-next-variation-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-next-variation.svg b/src/pentobi/icons/pentobi-next-variation.svg new file mode 100644 index 0000000..cc39628 --- /dev/null +++ b/src/pentobi/icons/pentobi-next-variation.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-piece-clear.svg b/src/pentobi/icons/pentobi-piece-clear.svg new file mode 100644 index 0000000..f8cdbd4 --- /dev/null +++ b/src/pentobi/icons/pentobi-piece-clear.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/icons/pentobi-play-16.svg b/src/pentobi/icons/pentobi-play-16.svg new file mode 100644 index 0000000..8d09c1c --- /dev/null +++ b/src/pentobi/icons/pentobi-play-16.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-play.svg b/src/pentobi/icons/pentobi-play.svg new file mode 100644 index 0000000..d860369 --- /dev/null +++ b/src/pentobi/icons/pentobi-play.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-previous-piece.svg b/src/pentobi/icons/pentobi-previous-piece.svg new file mode 100644 index 0000000..1180935 --- /dev/null +++ b/src/pentobi/icons/pentobi-previous-piece.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/icons/pentobi-previous-variation-16.svg b/src/pentobi/icons/pentobi-previous-variation-16.svg new file mode 100644 index 0000000..f98e8a0 --- /dev/null +++ b/src/pentobi/icons/pentobi-previous-variation-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-previous-variation.svg b/src/pentobi/icons/pentobi-previous-variation.svg new file mode 100644 index 0000000..2d1d3f8 --- /dev/null +++ b/src/pentobi/icons/pentobi-previous-variation.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-rated-game-16.svg b/src/pentobi/icons/pentobi-rated-game-16.svg new file mode 100644 index 0000000..6bfe814 --- /dev/null +++ b/src/pentobi/icons/pentobi-rated-game-16.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/pentobi/icons/pentobi-rated-game.svg b/src/pentobi/icons/pentobi-rated-game.svg new file mode 100644 index 0000000..8a714cf --- /dev/null +++ b/src/pentobi/icons/pentobi-rated-game.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/pentobi/icons/pentobi-rotate-left.svg b/src/pentobi/icons/pentobi-rotate-left.svg new file mode 100644 index 0000000..8043152 --- /dev/null +++ b/src/pentobi/icons/pentobi-rotate-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/icons/pentobi-rotate-right.svg b/src/pentobi/icons/pentobi-rotate-right.svg new file mode 100644 index 0000000..72aa153 --- /dev/null +++ b/src/pentobi/icons/pentobi-rotate-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/icons/pentobi-undo-16.svg b/src/pentobi/icons/pentobi-undo-16.svg new file mode 100644 index 0000000..991c9d4 --- /dev/null +++ b/src/pentobi/icons/pentobi-undo-16.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-undo.svg b/src/pentobi/icons/pentobi-undo.svg new file mode 100644 index 0000000..45bc84e --- /dev/null +++ b/src/pentobi/icons/pentobi-undo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi.svg b/src/pentobi/icons/pentobi.svg new file mode 100644 index 0000000..fea7ae6 --- /dev/null +++ b/src/pentobi/icons/pentobi.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/pentobi.conf.in b/src/pentobi/pentobi.conf.in new file mode 100644 index 0000000..e128c3b --- /dev/null +++ b/src/pentobi/pentobi.conf.in @@ -0,0 +1,6 @@ +# Config file to override installation settings such that the executable +# can be tested without installation +BooksDir=@CMAKE_SOURCE_DIR@/src/books +HelpDir=@CMAKE_SOURCE_DIR@/src/pentobi/help +TranslationsPentobiDir=@CMAKE_BINARY_DIR@/src/pentobi +TranslationsLibPentobiGuiDir=@CMAKE_BINARY_DIR@/src/libpentobi_gui diff --git a/src/pentobi/pentobi.ico b/src/pentobi/pentobi.ico new file mode 100644 index 0000000..a947864 Binary files /dev/null and b/src/pentobi/pentobi.ico differ diff --git a/src/pentobi/pentobi.rc b/src/pentobi/pentobi.rc new file mode 100644 index 0000000..3c6d0d5 --- /dev/null +++ b/src/pentobi/pentobi.rc @@ -0,0 +1 @@ + IDI_ICON1 ICON DISCARDABLE "pentobi.ico" diff --git a/src/pentobi/resources.qrc b/src/pentobi/resources.qrc new file mode 100644 index 0000000..0a8988a --- /dev/null +++ b/src/pentobi/resources.qrc @@ -0,0 +1,37 @@ + + + +icons/pentobi.png +icons/pentobi-16.png +icons/pentobi-32.png +icons/pentobi-backward.png +icons/pentobi-backward-16.png +icons/pentobi-beginning.png +icons/pentobi-beginning-16.png +icons/pentobi-computer-colors.png +icons/pentobi-computer-colors-16.png +icons/pentobi-end.png +icons/pentobi-end-16.png +icons/pentobi-flip-horizontal.png +icons/pentobi-flip-vertical.png +icons/pentobi-forward.png +icons/pentobi-forward-16.png +icons/pentobi-newgame.png +icons/pentobi-newgame-16.png +icons/pentobi-next-piece.png +icons/pentobi-next-variation.png +icons/pentobi-next-variation-16.png +icons/pentobi-piece-clear.png +icons/pentobi-play.png +icons/pentobi-play-16.png +icons/pentobi-previous-piece.png +icons/pentobi-previous-variation.png +icons/pentobi-previous-variation-16.png +icons/pentobi-rated-game.png +icons/pentobi-rated-game-16.png +icons/pentobi-rotate-left.png +icons/pentobi-rotate-right.png +icons/pentobi-undo.png +icons/pentobi-undo-16.png + + diff --git a/src/pentobi/resources_2x.qrc b/src/pentobi/resources_2x.qrc new file mode 100644 index 0000000..0af20b7 --- /dev/null +++ b/src/pentobi/resources_2x.qrc @@ -0,0 +1,37 @@ + + + +icons/pentobi@2x.png +icons/pentobi-16@2x.png +icons/pentobi-32@2x.png +icons/pentobi-backward@2x.png +icons/pentobi-backward-16@2x.png +icons/pentobi-beginning@2x.png +icons/pentobi-beginning-16@2x.png +icons/pentobi-computer-colors@2x.png +icons/pentobi-computer-colors-16@2x.png +icons/pentobi-end@2x.png +icons/pentobi-end-16@2x.png +icons/pentobi-flip-horizontal@2x.png +icons/pentobi-flip-vertical@2x.png +icons/pentobi-forward@2x.png +icons/pentobi-forward-16@2x.png +icons/pentobi-newgame@2x.png +icons/pentobi-newgame-16@2x.png +icons/pentobi-next-piece@2x.png +icons/pentobi-next-variation@2x.png +icons/pentobi-next-variation-16@2x.png +icons/pentobi-piece-clear@2x.png +icons/pentobi-play@2x.png +icons/pentobi-play-16@2x.png +icons/pentobi-previous-piece@2x.png +icons/pentobi-previous-variation@2x.png +icons/pentobi-previous-variation-16@2x.png +icons/pentobi-rated-game@2x.png +icons/pentobi-rated-game-16@2x.png +icons/pentobi-rotate-left@2x.png +icons/pentobi-rotate-right@2x.png +icons/pentobi-undo@2x.png +icons/pentobi-undo-16@2x.png + + diff --git a/src/pentobi/translations/pentobi.ts b/src/pentobi/translations/pentobi.ts new file mode 100644 index 0000000..60127d0 --- /dev/null +++ b/src/pentobi/translations/pentobi.ts @@ -0,0 +1,14 @@ + + + + + MainWindow + + %n move(s) + + %n move + %n moves + + + + diff --git a/src/pentobi/translations/pentobi_de.ts b/src/pentobi/translations/pentobi_de.ts new file mode 100644 index 0000000..029a4c8 --- /dev/null +++ b/src/pentobi/translations/pentobi_de.ts @@ -0,0 +1,1116 @@ + + + + + AnalyzeGameWidget + + Win + Gewinn + + + Loss + Verlust + + + Running game analysis... + Spiel wird analysiert ... + + + + AnalyzeGameWindow + + Game Analysis + Spielanalyse + + + + AnalyzeSpeedDialog + + Fast + Schnell + + + Normal + Normal + + + Slow + Langsam + + + Analysis speed: + Analysegeschwindigkeit: + + + + MainWindow + + About Pentobi + Über Pentobi + + + Next &Color + Nächste &Farbe + + + S&etup Mode + &Stellungsaufbau + + + Could not read file '%1' + Datei '%1' konnte nicht gelesen werden + + + The file is not a valid Blokus SGF file. + Die Datei ist keine gültige Blokus-SGF-Datei. + + + Truncate this subtree? + Diesen Teilbaum abschneiden? + + + This position and all following moves and variations will be removed from the game tree. + Diese Brettstellung und alle folgenden Züge und Varianten werden aus dem Spielbaum entfernt. + + + Truncate + Abschneiden + + + Truncate children? + Kindknoten abschneiden? + + + All following moves and variations will be removed from the game tree. + Alle folgenden Züge und Varianten werden aus dem Spielbaum entfernt. + + + Truncate Children + Kindknoten abschneiden + + + Could not delete %1 + %1 konnte nicht gelöscht werden + + + Rated game + Gewertetes Spiel + + + Continuing unfinished rated game. + Unbeendetes gewertetes Spiel wird fortgesetzt. + + + You play %1 in this game. + Sie spielen %1 in diesem Spiel. + + + &copy; 2011&ndash;%1 Markus Enzenberger + &copy; 2011&ndash;%1 Markus Enzenberger + + + Analyze Game + Spiel analysieren + + + &About Pentobi + Über &Pentobi + + + &Analyze Game... + Spiel &analysieren ... + + + B&ackward + &Zurück + + + Go one move backward + Einen Zug zurück gehen + + + Back to &Main Variation + Zurück zu Hau&ptvariante + + + &Bad + Schl&echt + + + &Beginning + &Anfang + + + Go to beginning of game + Zum Anfang des Spiels gehen + + + Beginning of Bran&ch + Anfang der Verz&weigung + + + Clear Piece + Spielstein löschen + + + &Computer Colors + &Computer-Farben + + + Set the colors played by the computer + Die vom Computer gespielten Farben festlegen + + + C&oordinates + K&oordinaten + + + &Delete All Variations + Alle &Varianten löschen + + + &Doubtful + &Zweifelhaft + + + &End + &Ende + + + Go to end of moves + Zum Ende der Züge gehen + + + &ASCII Art + &ASCII-Art + + + I&mage + &Grafik + + + &Find Move + Zug fin&den + + + Flip Horizontally + Waagrecht umdrehen + + + Flip Vertically + Senkrecht umdrehen + + + &Forward + &Vorwärts + + + Go one move forward + Einen Zug vorwärts gehen + + + &Fullscreen + Voll&bild + + + Ga&me Info + Spielinf&ormation + + + St&op + St&opp + + + &Duo + &Duo + + + &Good + &Gut + + + Game analysis is only possible in the main variation. + Spielanalyse ist nur in der Hauptvariante möglich. + + + Find Next &Comment + Nächsten &Kommentar finden + + + &Go to Move... + &Gehe zu Zug ... + + + Pentobi &Help + Pentobi-&Hilfe + + + I&nteresting + I&nteressant + + + &Keep Only Position + Nur &Brettstellung behalten + + + Keep Only &Subtree + Nur &Teilbaum behalten + + + Leave Fullscreen + Vollbild verlassen + + + M&ake Main Variation + Zu &Hauptvariante machen + + + Move Variation D&own + Variante nach &unten schieben + + + Move Variation &Up + Variante nach &oben schieben + + + &1 + &1 + + + &2 + &2 + + + &3 + &3 + + + &4 + &4 + + + &5 + &5 + + + &6 + &6 + + + &7 + &7 + + + &8 + &8 + + + &None + move numbers + &Keine + + + Next Piece + Nächster Spielstein + + + &Next Variation + &Nächste Variante + + + Go to next variation + Zur nächsten Variante gehen + + + &Rated Game + Ge&wertetes Spiel + + + &New + &Neu + + + Start a new game + Ein neues Spiel beginnen + + + N&one + move annotation + &Keine + + + &Open... + Öffn&en ... + + + &Play + &Spielen + + + Play &Single Move + &Einzelnen Zug spielen + + + Previous Piece + Vorheriger Spielstein + + + &Previous Variation + Vor&herige Variante + + + Go to previous variation + Zur vorherigen Variante gehen + + + Rotate Anticlockwise + Gegen den Uhrzeigersinn drehen + + + Rotate Clockwise + Im Uhrzeigersinn drehen + + + &Quit + &Beenden + + + &Save + &Speichern + + + Save &As... + Speichern &unter ... + + + &Comment + &Kommentar + + + &Rating + &Wertung + + + &No Text + &Kein Text + + + Text &Beside Icons + Text n&eben Symbolen + + + Text Bel&ow Icons + Text &unter Symbolen + + + &Text Only + &Nur Text + + + &System Default + &Systemvorgabe + + + &Truncate + &Abschneiden + + + Truncate C&hildren + &Kindknoten abschneiden + + + Show &Variations + &Varianten zeigen + + + &Undo Move + Zug rück&gängig + + + V&ery Bad + Seh&r schlecht + + + &Very Good + &Sehr gut + + + &Edit + &Bearbeiten + + + &Move Annotation + &Zugkommentierung + + + &View + &Ansicht + + + Toolbar T&ext + Werkzeugleistent&ext + + + &Computer + &Computer + + + &Tools + E&xtras + + + &Help + &Hilfe + + + Delete all variations? + Alle Varianten löschen? + + + All variations but the main variation will be removed from the game tree. + Alle Varianten außer der Hauptvariante werden aus dem Spielbaum entfernt. + + + Delete Variations + Varianten löschen + + + &Toolbar + &Werkzeugleiste + + + Text files (*.txt);;All files (*) + Textdateien (*.txt);;Alle Dateien (*) + + + No comment found + Kein Kommentar gefunden + + + Blokus games (*.blksgf);;All files (*) + Blokus-Partien (*.blksgf);;Alle Dateien (*) + + + Move number: + Zugnummer: + + + Go to Move + Gehe zu Zug + + + Keep only position? + Nur Brettstellung behalten? + + + All previous and following moves and variations will be removed from the game tree. + Alle vorhergehenden und nachfolgenden Züge und Varianten werden aus dem Spielbaum entfernt. + + + Keep only subtree? + Nur Teilbaum behalten? + + + All previous moves and variations will be removed from the game tree. + Alle vorhergehenden Züge und Varianten werden aus dem Spielbaum entfernt. + + + Start rated game? + Gewertetes Spiel beginnen? + + + In this game, you play %1 against Pentobi level&nbsp;%2. + In diesem Spiel spielen Sie %1 gegen Pentobi Spielstufe&nbsp;%2. + + + &Start Game + &Spiel beginnen + + + Pentobi %1 (level %2) + The first argument is the version of Pentobi + Pentobi %1 (Stufe %2) + + + Human + Mensch + + + Open + Öffnen + + + The file could not be saved. + Die Datei konnte nicht gespeichert werden. + + + %1: %2 + Error message if file cannot be saved. %1 is replaced by the file name, %2 by the error message of the operating system. + %1: %2 + + + Untitled Game.blksgf + Unbenanntes Spiel.blksgf + + + Untitled Game %1.blksgf + Unbenanntes Spiel %1.blksgf + + + Save + Speichern + + + Pentobi + Pentobi + + + Version %1 + Version %1 + + + Computer opponent for the board game Blokus. + Computer-Gegner für das Brettspiel Blokus. + + + The file has been modified. + Die Datei wurde geändert. + + + Do you want to save your changes? + Änderungen speichern? + + + &Don't Save + &Nicht speichern + + + The current game is not finished. + Das gegenwärtige Spiel ist nicht beendet. + + + Do you want to abort the game? + Möchten Sie das Spiel verwerfen? + + + &Abort Game + Spiel &verwerfen + + + &9 + &9 + + + &All with Number + &Alle mit Nummer + + + Last with &Dot + Letzter mit &Punkt + + + &Last with Number + Letzter mit &Nummer + + + Start a rated game + Ein gewertetes Spiel beginnen + + + Classic (&3 Players) + Klassisch (&3 Spieler) + + + &Game + &Spiel + + + Game &Variant + Spiel&variante + + + Open R&ecent + &Zuletzt benutzte Dateien + + + E&xport + E&xportieren + + + G&o + &Gehe zu + + + &Move Marking + &Zugmarkierung + + + Export Image + Grafik exportieren + + + Image size: + Bildgröße: + + + The end of the tree was reached. + Das Ende des Spielbaums wurde erreicht. + + + Continue the search from the start of the tree? + Die Suche vom Beginn des Spielbaums fortsetzen? + + + Continue From Start + Suche vom Beginn fortsetzen + + + Show &Rating + &Wertung zeigen + + + Pentobi Help + Pentobi-Hilfe + + + Keep Only Position + Nur Brettstellung behalten + + + Keep Only Subtree + Nur Teilbaum behalten + + + [*]%1 + [*]%1 + + + Setup mode cannot be used if moves have been played. + Stellungsaufbau-Modus kann nicht benutzt werden, wenn Züge gespielt wurden. + + + Setup mode + Stellungsaufbau + + + The game ends in a tie. + Das Spiel endet in einem Unentschieden. + + + The game ends in a tie between all colors. + Das Spiel endet in einem Unentschieden zwischen allen Farben. + + + The game ends in a tie between Blue, Yellow and Red. + Das Spiel endet in einem Unentschieden zwischen Blau, Gelb und Rot. + + + The game ends in a tie between Blue, Yellow and Green. + Das Spiel endet in einem Unentschieden zwischen Blau, Gelb und Grün. + + + The game ends in a tie between Blue, Red and Green. + Das Spiel endet in einem Unentschieden zwischen Blau, Rot und Grün. + + + The game ends in a tie between Yellow, Red and Green. + Das Spiel endet in einem Unentschieden zwischen Gelb, Rot und Grün. + + + The game ends in a tie between Blue and Yellow. + Das Spiel endet in einem Unentschieden zwischen Blau und Gelb. + + + The game ends in a tie between Blue and Red. + Das Spiel endet in einem Unentschieden zwischen Blau und Rot. + + + The game ends in a tie between Blue and Green. + Das Spiel endet in einem Unentschieden zwischen Blau und Grün. + + + The game ends in a tie between Yellow and Red. + Das Spiel endet in einem Unentschieden zwischen Gelb und Rot. + + + The game ends in a tie between Yellow and Green. + Das Spiel endet in einem Unentschieden zwischen Gelb und Grün. + + + The game ends in a tie between Red and Green. + Das Spiel endet in einem Unentschieden zwischen Rot und Grün. + + + Blue wins. + Blau gewinnt. + + + Yellow wins. + Gelb gewinnt. + + + Red wins. + Rot gewinnt. + + + Green wins. + Grün gewinnt. + + + Your rating has increased from %1 to %2. + Ihre Wertung hat sich von %1 auf %2 erhöht. + + + Your rating stays at %1. + Ihre Wertung bleibt auf %1. + + + Your rating has decreased from %1 to %2. + Ihre Wertung hat sich von %1 auf %2 erniedrigt. + + + Error in file '%1' + Fehler in Datei '%1' + + + Move %1 + Zug %1 + + + %n move(s) + + %n Zug + %n Züge + + + + Move %1 of %2 + Zug %1 von %2 + + + Move %1 of %2 in variation %3 + Zug %1 von %2 in Variante %3 + + + Make the computer continue to play the current color + Den Computer die gegenwärtige Farbe weiterspielen lassen + + + Make the computer play the current color + Den Computer die gegenwärtige Farbe spielen lassen + + + Classic (&4 Players) + Klassisch (&4 Spieler) + + + J&unior + J&unior + + + Classic (&2 Players) + Klassisch (&2 Spieler) + + + Nexos (&2 Players) + Nexos (&2 Spieler) + + + Nexos (&4 Players) + Nexos (&4 Spieler) + + + Trigon (&2 Players) + Trigon (&2 Spieler) + + + Trigon (&3 Players) + Trigon (&3 Spieler) + + + Trigon (&4 Players) + Trigon (&4 Spieler) + + + &Classic + &Klassisch + + + &Trigon + &Trigon + + + &Nexos + &Nexos + + + Blue wins with 1 point. + Blau gewinnt mit 1 Punkt. + + + Blue wins with %1 points. + Blau gewinnt mit %1 Punkten. + + + Green wins with 1 point. + Grün gewinnt mit 1 Punkt. + + + Green wins with %1 points. + Grün gewinnt mit %1 Punkten. + + + Blue/Red wins with 1 point. + Blau/Rot gewinnt mit 1 Punkt. + + + Blue/Red wins with %1 points. + Blau/Rot gewinnt mit %1 Punkten. + + + Yellow/Green wins with 1 point. + Gelb/Grün gewinnt mit 1 Punkt. + + + Yellow/Green wins with %1 points. + Gelb/Grün gewinnt mit %1 Punkten. + + + Callisto (&2 Players) + Callisto (&2 Spieler) + + + Callisto (&3 Players) + Callisto (&3 Spieler) + + + Callisto (&4 Players) + Callisto (&4 Spieler) + + + C&allisto + &Callisto + + + Green wins (tie resolved). + Grün gewinnt (Unentschieden aufgelöst). + + + Yellow/Green wins (tie resolved). + Gelb/Grün gewinnt (Unentschieden aufgelöst). + + + Red wins (tie resolved). + Rot gewinnt (Unentschieden aufgelöst). + + + Yellow wins (tie resolved). + Gelb gewinnt (Unentschieden aufgelöst). + + + &Level (Classic, 4 Players) + Spielst&ufe (Klassisch, 4 Spieler) + + + &Level (Classic, 2 Players) + Spielst&ufe (Klassisch, 2 Spieler) + + + &Level (Classic, 3 Players) + Spielst&ufe (Klassisch, 3 Spieler) + + + &Level (Duo) + Spielst&ufe (Duo) + + + &Level (Trigon, 4 Players) + Spielst&ufe (Trigon, 4 Spieler) + + + &Level (Trigon, 2 Players) + Spielst&ufe (Trigon, 2 Spieler) + + + &Level (Trigon, 3 Players) + Spielst&ufe (Trigon, 3 Spieler) + + + &Level (Junior) + Spielst&ufe (Junior) + + + &Level (Nexos, 4 Players) + Spielst&ufe (Nexos, 4 Spieler) + + + &Level (Nexos, 2 Players) + Spielst&ufe (Nexos, 2 Spieler) + + + &Level (Callisto, 4 Players) + Spielst&ufe (Callisto, 4 Spieler) + + + &Level (Callisto, 2 Players) + Spielst&ufe (Callisto, 2 Spieler) + + + &Level (Callisto, 3 Players) + Spielst&ufe (Callisto, 3 Spieler) + + + Computer is thinking... (up to %1 seconds remaining) + Computer denkt ... (maximal %1 Sekunden verbleibend) + + + Computer is thinking... (up to %1 minutes remaining) + Computer denkt ... (maximal %1 Minuten verbleibend) + + + Computer is thinking... + Computer denkt ... + + + + RatedGamesList + + Game + Spiel + + + Your Color + Ihre Farbe + + + Level + Stufe + + + Result + Ergebnis + + + Date + Datum + + + Win + Gewinn + + + Tie + Unentschieden + + + Loss + Verlust + + + + RatingDialog + + Rating + Wertung + + + Your rating: + Ihre Wertung: + + + Game variant: + Spielvariante: + + + Number rated games: + Anzahl gewertete Spiele: + + + Best previous rating: + Beste frühere Wertung: + + + Recent games: + Zuletzt gespielte Spiele: + + + &Clear + &Löschen + + + Clear rating and delete rating history? + Wertung und Wertungsentwicklung löschen? + + + Clear rating + Wertung löschen + + + Classic (4 players) + Klassisch (4 Spieler) + + + Classic (2 players) + Klassisch (2 Spieler) + + + Classic (3 players) + Klassisch (3 Spieler) + + + Duo + Duo + + + Trigon (4 players) + Trigon (4 Spieler) + + + Trigon (2 players) + Trigon (2 Spieler) + + + Trigon (3 players) + Trigon (3 Spieler) + + + Junior + Junior + + + Recent development: + Aktuelle Entwicklung: + + + Nexos (4 players) + Nexos (4 Spieler) + + + Nexos (2 players) + Nexos (2 Spieler) + + + Callisto (4 players) + Callisto (4 Spieler) + + + Callisto (2 players) + Callisto (2 Spieler) + + + Callisto (3 players) + Callisto (3 Spieler) + + + + main + + Not enough memory. + Nicht genügend Speicher. + + + Pentobi + Pentobi + + + diff --git a/src/pentobi_gtp/CMakeLists.txt b/src/pentobi_gtp/CMakeLists.txt new file mode 100644 index 0000000..d42d74c --- /dev/null +++ b/src/pentobi_gtp/CMakeLists.txt @@ -0,0 +1,24 @@ +add_executable(pentobi-gtp + Engine.h + Engine.cpp + Main.cpp +) + +target_link_libraries(pentobi-gtp + pentobi_mcts + pentobi_base + boardgame_base + boardgame_sgf + boardgame_util + boardgame_sys + boardgame_gtp + ) + +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(pentobi-gtp ${CMAKE_THREAD_LIBS_INIT}) +endif() + +if(MINGW AND (CMAKE_SIZEOF_VOID_P EQUAL "4")) + set_target_properties(pentobi-gtp PROPERTIES LINK_FLAGS -Wl,--large-address-aware) +endif() + diff --git a/src/pentobi_gtp/Engine.cpp b/src/pentobi_gtp/Engine.cpp new file mode 100644 index 0000000..07734c3 --- /dev/null +++ b/src/pentobi_gtp/Engine.cpp @@ -0,0 +1,183 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_gtp/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Engine.h" + +#include +#include "libpentobi_mcts/Util.h" + +namespace pentobi_gtp { + +using libboardgame_gtp::Failure; +using libpentobi_mcts::Float; + +//----------------------------------------------------------------------------- + +Engine::Engine(Variant variant, unsigned level, bool use_book, + const string& books_dir, unsigned nu_threads) + : libpentobi_base::Engine(variant) +{ + create_player(variant, level, books_dir, nu_threads); + get_mcts_player().set_use_book(use_book); + add("get_value", &Engine::cmd_get_value); + add("name", &Engine::cmd_name); + add("param", &Engine::cmd_param); + add("move_values", &Engine::cmd_move_values); + add("save_tree", &Engine::cmd_save_tree); + add("version", &Engine::cmd_version); +} + +Engine::~Engine() = default; + +void Engine::cmd_get_value(Response& response) +{ + response << get_search().get_tree().get_root().get_value(); +} + +void Engine::cmd_move_values(Response& response) +{ + auto& search = get_search(); + auto& tree = search.get_tree(); + auto& bd = get_board(); + vector children; + for (auto& i : tree.get_root_children()) + children.push_back(&i); + sort(children.begin(), children.end(), libpentobi_mcts::util::compare_node); + response << fixed; + for (auto node : children) + response << setprecision(0) << node->get_visit_count() << ' ' + << setprecision(1) << node->get_value_count() << ' ' + << setprecision(3) << node->get_value() << ' ' + << bd.to_string(node->get_move(), true) << '\n'; +} + +void Engine::cmd_name(Response& response) +{ + response.set("Pentobi"); +} + +void Engine::cmd_save_tree(const Arguments& args) +{ + auto& search = get_search(); + if (! search.get_last_history().is_valid()) + throw Failure("no search tree"); + ofstream out(args.get()); + libpentobi_mcts::util::dump_tree(out, search); +} + +void Engine::cmd_param(const Arguments& args, Response& response) +{ + auto& p = get_mcts_player(); + auto& s = get_search(); + if (args.get_size() == 0) + response + << "avoid_symmetric_draw " << s.get_avoid_symmetric_draw() << '\n' + << "auto_param " << s.get_auto_param() << '\n' + << "exploration_constant " << s.get_exploration_constant() << '\n' + << "expand_threshold " << s.get_expand_threshold() << '\n' + << "expand_threshold_inc " << s.get_expand_threshold_inc() << '\n' + << "fixed_simulations " << p.get_fixed_simulations() << '\n' + << "rave_child_max " << s.get_rave_child_max() << '\n' + << "rave_parent_max " << s.get_rave_parent_max() << '\n' + << "rave_weight " << s.get_rave_weight() << '\n' + << "reuse_subtree " << s.get_reuse_subtree() << '\n' + << "use_book " << p.get_use_book() << '\n'; + else + { + args.check_size(2); + string name = args.get(0); + if (name == "avoid_symmetric_draw") + s.set_avoid_symmetric_draw(args.parse(1)); + else if (name == "auto_param") + s.set_auto_param(args.parse(1)); + else if (name == "exploration_constant") + s.set_exploration_constant(args.parse(1)); + else if (name == "expand_threshold") + s.set_expand_threshold(args.parse(1)); + else if (name == "expand_threshold_inc") + s.set_expand_threshold_inc(args.parse(1)); + else if (name == "fixed_simulations") + p.set_fixed_simulations(args.parse(1)); + else if (name == "rave_child_max") + s.set_rave_child_max(args.parse(1)); + else if (name == "rave_parent_max") + s.set_rave_parent_max(args.parse(1)); + else if (name == "rave_weight") + s.set_rave_weight(args.parse(1)); + else if (name == "reuse_subtree") + s.set_reuse_subtree(args.parse(1)); + else if (name == "use_book") + p.set_use_book(args.parse(1)); + else + { + ostringstream msg; + msg << "unknown parameter '" << name << "'"; + throw Failure(msg.str()); + } + } +} + +void Engine::cmd_version(Response& response) +{ + string version; +#ifdef VERSION + version = VERSION; +#endif + if (version.empty()) + version = "UNKNOWN"; + // By convention, the version string of unreleased versions contains the + // string UNKNOWN (appended to the last released version). In this case, or + // if VERSION was undefined, we append the build date. + if (version.find("UNKNOWN") != string::npos) + { + version.append(" ("); + version.append(__DATE__); + version.append(")"); + } +#if LIBBOARDGAME_DEBUG + version.append(" (dbg)"); +#endif + response.set(version); +} + +void Engine::create_player(Variant variant, unsigned level, + const string& books_dir, unsigned nu_threads) +{ + auto max_level = level; + m_player.reset(new Player(variant, max_level, books_dir, nu_threads)); + get_mcts_player().set_level(level); + set_player(*m_player); +} + +Player& Engine::get_mcts_player() +{ + try + { + return dynamic_cast(*m_player); + } + catch (const bad_cast&) + { + throw Failure("current player is not mcts player"); + } +} + +Search& Engine::get_search() +{ + return get_mcts_player().get_search(); +} + +void Engine::use_cpu_time(bool enable) +{ + get_mcts_player().use_cpu_time(enable); +} + +//----------------------------------------------------------------------------- + +} // namespace pentobi_gtp diff --git a/src/pentobi_gtp/Engine.h b/src/pentobi_gtp/Engine.h new file mode 100644 index 0000000..76418b3 --- /dev/null +++ b/src/pentobi_gtp/Engine.h @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_gtp/Engine.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_GTP_ENGINE_H +#define PENTOBI_GTP_ENGINE_H + +#include "libpentobi_base/Engine.h" +#include "libpentobi_mcts/Player.h" + +namespace pentobi_gtp { + +using namespace std; +using libboardgame_gtp::Arguments; +using libboardgame_gtp::Response; +using libpentobi_base::PlayerBase; +using libpentobi_base::Variant; +using libpentobi_mcts::Player; +using libpentobi_mcts::Search; + +//----------------------------------------------------------------------------- + +class Engine + : public libpentobi_base::Engine +{ +public: + Engine(Variant variant, unsigned level = 5, + bool use_book = true, const string& books_dir = "", + unsigned nu_threads = 0); + + ~Engine(); + + void cmd_param(const Arguments&, Response&); + void cmd_get_value(Response&); + void cmd_move_values(Response&); + void cmd_name(Response&); + void cmd_save_tree(const Arguments&); + void cmd_version(Response&); + + Player& get_mcts_player(); + + /** @see Player::use_cpu_time() */ + void use_cpu_time(bool enable); + +private: + unique_ptr m_player; + + void create_player(Variant variant, unsigned level, + const string& books_dir, unsigned nu_threads); + + Search& get_search(); +}; + +//----------------------------------------------------------------------------- + +} // namespace pentobi_gtp + +#endif // PENTOBI_GTP_ENGINE_H diff --git a/src/pentobi_gtp/Main.cpp b/src/pentobi_gtp/Main.cpp new file mode 100644 index 0000000..3fa9087 --- /dev/null +++ b/src/pentobi_gtp/Main.cpp @@ -0,0 +1,170 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_gtp/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "Engine.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/Options.h" +#include "libboardgame_util/RandomGenerator.h" + +using namespace std; +using libboardgame_gtp::Failure; +using libboardgame_util::Options; +using libboardgame_util::RandomGenerator; +using libpentobi_base::parse_variant_id; +using libpentobi_base::Board; +using libpentobi_base::Variant; +using libpentobi_mcts::Player; + +//----------------------------------------------------------------------------- + +namespace { + +string get_application_dir_path(int argc, char** argv) +{ + if (argc == 0 || ! argv || ! argv[0]) + return ""; + string application_path(argv[0]); +#ifdef _WIN32 + auto pos = application_path.find_last_of("/\\"); +#else + auto pos = application_path.find_last_of("/"); +#endif + if (pos == string::npos) + return ""; + return application_path.substr(0, pos); +} + +} // namespace + +//----------------------------------------------------------------------------- + +int main(int argc, char** argv) +{ + string application_dir_path = get_application_dir_path(argc, argv); + try + { + vector specs = { + "book:", + "config|c:", + "color", + "cputime", + "game|g:", + "help|h", + "level|l:", + "nobook", + "noresign", + "quiet|q", + "seed|r:", + "showboard", + "threads:", + "version|v" + }; + Options opt(argc, argv, specs); + if (opt.contains("help")) + { + cout << + "Usage: pentobi_gtp [options] [input files]\n" + "--book load an external book file\n" + "--config,-c set GTP config file\n" + "--color colorize text output of boards\n" + "--cputime use CPU time\n" + "--game,-g game variant (classic, classic_2, classic_3,\n" + " duo, trigon, trigon_2, trigon_3, junior)\n" + "--help,-h print help message and exit\n" + "--level,-l set playing strength level\n" + "--seed,-r set random seed\n" + "--showboard automatically write board to stderr after\n" + " changes\n" + "--nobook disable opening book\n" + "--noresign disable resign\n" + "--quiet,-q do not print logging messages\n" + "--threads number of threads in the search\n" + "--version,-v print version and exit\n"; + return 0; + } + if (opt.contains("version")) + { +#ifdef VERSION + cout << "Pentobi " << VERSION << '\n'; +#else + cout << "Pentobi UNKNONW"; +#endif + return 0; + } + unsigned threads = 1; + if (opt.contains("threads")) + { + threads = opt.get("threads"); + if (threads == 0) + throw runtime_error("Number of threads must be greater zero."); + } + Board::color_output = opt.contains("color"); + if (opt.contains("quiet")) + libboardgame_util::disable_logging(); + if (opt.contains("seed")) + RandomGenerator::set_global_seed( + opt.get("seed")); + string variant_string = opt.get("game", "classic"); + Variant variant; + if (! parse_variant_id(variant_string, variant)) + throw runtime_error("invalid game variant " + variant_string); + auto level = opt.get("level", 4); + if (level < 1 || level > Player::max_supported_level) + throw runtime_error("invalid level"); + auto use_book = (! opt.contains("nobook")); + string books_dir = application_dir_path; + pentobi_gtp::Engine engine(variant, level, use_book, books_dir, + threads); + engine.set_resign(! opt.contains("noresign")); + if (opt.contains("showboard")) + engine.set_show_board(true); + if (opt.contains("cputime")) + engine.use_cpu_time(true); + string book_file = opt.get("book", ""); + if (! book_file.empty()) + { + ifstream in(book_file); + engine.get_mcts_player().load_book(in); + } + string config_file = opt.get("config", ""); + if (! config_file.empty()) + { + ifstream in(config_file); + if (! in) + throw runtime_error("Error opening " + config_file); + engine.exec(in, true, libboardgame_util::get_log_stream()); + } + auto& args = opt.get_args(); + if (! args.empty()) + for (auto& file : args) + { + ifstream in(file); + if (! in) + throw runtime_error("Error opening " + file); + engine.exec_main_loop(in, cout); + } + else + engine.exec_main_loop(cin, cout); + return 0; + } + catch (const Failure& e) + { + LIBBOARDGAME_LOG("Error: command in config file failed: ", e.what()); + return 1; + } + catch (const exception& e) + { + LIBBOARDGAME_LOG("Error: ", e.what()); + return 1; + } +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_kde_thumbnailer/CMakeLists.txt b/src/pentobi_kde_thumbnailer/CMakeLists.txt new file mode 100644 index 0000000..bc3002f --- /dev/null +++ b/src/pentobi_kde_thumbnailer/CMakeLists.txt @@ -0,0 +1,23 @@ +find_package(ECM REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + +include(KDEInstallDirs) +include(KDECompilerSettings) +include(KDECMakeSettings) + +find_package(KF5 REQUIRED COMPONENTS KIO) + +include_directories(${CMAKE_SOURCE_DIR}/src) + +add_library(pentobi-thumbnail MODULE + PentobiThumbCreator.h + PentobiThumbCreator.cpp +) + +target_link_libraries(pentobi-thumbnail + pentobi_kde_thumbnailer + KF5::KIOWidgets +) + +install(TARGETS pentobi-thumbnail DESTINATION ${PLUGIN_INSTALL_DIR}) +install(FILES pentobi-thumbnail.desktop DESTINATION ${SERVICES_INSTALL_DIR}) diff --git a/src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp new file mode 100644 index 0000000..ce003d6 --- /dev/null +++ b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_kde_thumbnailer/PentobiThumbCreator.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "PentobiThumbCreator.h" + +#include +#include "libpentobi_thumbnail/CreateThumbnail.h" + +//----------------------------------------------------------------------------- + +extern "C" { + +Q_DECL_EXPORT ThumbCreator* new_creator() { return new PentobiThumbCreator; } + +} + +//----------------------------------------------------------------------------- + +PentobiThumbCreator::~PentobiThumbCreator() = default; + +bool PentobiThumbCreator::create(const QString& path, int width, int height, + QImage& image) +{ + image = QImage(width, height, QImage::Format_ARGB32); + if (image.isNull()) + return false; + image.fill(Qt::transparent); + return createThumbnail(path, width, height, image); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_kde_thumbnailer/PentobiThumbCreator.h b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.h new file mode 100644 index 0000000..a28b14d --- /dev/null +++ b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.h @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_kde_thumbnailer/PentobiThumbCreator.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_KDE_THUMBNAILER_PENTOBI_THUMB_CREATOR_H +#define PENTOBI_KDE_THUMBNAILER_PENTOBI_THUMB_CREATOR_H + +#include +#include + +//----------------------------------------------------------------------------- + +class PentobiThumbCreator + : public QObject, + public ThumbCreator +{ + Q_OBJECT + +public: + virtual ~PentobiThumbCreator(); + + bool create(const QString& path, int width, int height, + QImage& image) override; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_KDE_THUMBNAILER_PENTOBI_THUMB_CREATOR_H diff --git a/src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop b/src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop new file mode 100644 index 0000000..1acd6dc --- /dev/null +++ b/src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Service +Name=Blokus games +Name[de]=Blokus-Partien +ServiceTypes=ThumbCreator +MimeType=application/x-blokus-sgf; +X-KDE-Library=pentobi-thumbnail diff --git a/src/pentobi_qml/.gitignore b/src/pentobi_qml/.gitignore new file mode 100644 index 0000000..9bfc6e8 --- /dev/null +++ b/src/pentobi_qml/.gitignore @@ -0,0 +1 @@ +Pentobi.pro.user diff --git a/src/pentobi_qml/CMakeLists.txt b/src/pentobi_qml/CMakeLists.txt new file mode 100644 index 0000000..36db59d --- /dev/null +++ b/src/pentobi_qml/CMakeLists.txt @@ -0,0 +1,41 @@ +set(CMAKE_AUTOMOC TRUE) + +include_directories(${CMAKE_SOURCE_DIR}/src) + +set(pentobi_qml_SRCS + GameModel.h + GameModel.cpp + Main.cpp + PieceModel.h + PieceModel.cpp + PlayerModel.h + PlayerModel.cpp +) + +qt5_add_resources(pentobi_qml_RC_SRCS + resources.qrc + qml/themes/theme_light.qrc + qml/themes/theme_shared.qrc + ../books/pentobi_books.qrc + ../pentobi/icons.qrc +) + +add_executable(pentobi_qml WIN32 + ${pentobi_qml_SRCS} + ${pentobi_qml_RC_SRCS} +) + +target_link_libraries(pentobi_qml + pentobi_mcts + pentobi_base + boardgame_base + boardgame_sgf + boardgame_util + boardgame_sys +) + +qt5_use_modules(pentobi_qml Concurrent Gui Qml Svg) + +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(pentobi_qml ${CMAKE_THREAD_LIBS_INIT}) +endif() diff --git a/src/pentobi_qml/GameModel.cpp b/src/pentobi_qml/GameModel.cpp new file mode 100644 index 0000000..28cf24b --- /dev/null +++ b/src/pentobi_qml/GameModel.cpp @@ -0,0 +1,807 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/GameModel.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "GameModel.h" + +#include +#include +#include +#include +#include +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libpentobi_base/PentobiTreeWriter.h" +#include "libpentobi_base/TreeUtil.h" + +using namespace std; +using libboardgame_sgf::InvalidTree; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::back_to_main_variation; +using libboardgame_sgf::util::get_last_node; +using libboardgame_sgf::util::is_main_variation; +using libpentobi_base::get_piece_set; +using libpentobi_base::to_string_id; +using libpentobi_base::BoardType; +using libpentobi_base::Color; +using libpentobi_base::ColorMap; +using libpentobi_base::ColorMove; +using libpentobi_base::CoordPoint; +using libpentobi_base::MovePoints; +using libpentobi_base::PentobiTree; +using libpentobi_base::PentobiTreeWriter; +using libpentobi_base::Piece; +using libpentobi_base::PieceInfo; +using libpentobi_base::PiecePoints; +using libpentobi_base::PieceSet; +using libpentobi_base::Point; +using libpentobi_base::tree_util::get_position_info; + +//----------------------------------------------------------------------------- + +namespace { + +// Game coordinates are fractional because they refer to the center of a piece. +// This function is used to compare game coordinates of moves with the same +// piece, so we could even compare the rounded values (?), but comparing +// against epsilon is also safe. +bool compareGameCoord(const QPointF& p1, const QPointF& p2) +{ + return (p1 - p2).manhattanLength() < 0.01f; +} + +bool compareTransform(const PieceInfo& pieceInfo, const Transform* t1, + const Transform* t2) +{ + return pieceInfo.get_equivalent_transform(t1) == + pieceInfo.get_equivalent_transform(t2); +} + +QPointF getGameCoord(const Board& bd, Move mv) +{ + auto& geo = bd.get_geometry(); + PiecePoints movePoints; + for (Point p : bd.get_move_points(mv)) + movePoints.push_back(CoordPoint(geo.get_x(p), geo.get_y(p))); + return PieceModel::findCenter(bd, movePoints, false); +} + +} //namespace + +//----------------------------------------------------------------------------- + +GameModel::GameModel(QObject* parent) + : QObject(parent), + m_game(getInitialGameVariant()), + m_gameVariant(to_string_id(m_game.get_variant())), + m_nuColors(getBoard().get_nu_colors()) +{ + createPieceModels(); + updateProperties(); +} + +void GameModel::autoSave() +{ + // Don't autosave if game was not modified because it could have been + // loaded from a file, but autosave if not modified and empty to ensure + // that we start with the same game variant next time. + if (! m_game.is_modified() + && ! libboardgame_sgf::util::is_empty(m_game.get_tree())) + return; + ostringstream s; + PentobiTreeWriter writer(s, m_game.get_tree()); + writer.set_indent(-1); + writer.write(); + QSettings settings; + settings.setValue("variant", to_string_id(m_game.get_variant())); + settings.setValue("autosave", s.str().c_str()); +} + +void GameModel::backToMainVar() +{ + gotoNode(back_to_main_variation(m_game.get_current())); +} + +void GameModel::createPieceModels() +{ + createPieceModels(Color(0), m_pieceModels0); + createPieceModels(Color(1), m_pieceModels1); + if (m_nuColors > 2) + createPieceModels(Color(2), m_pieceModels2); + else + m_pieceModels2.clear(); + if (m_nuColors > 3) + createPieceModels(Color(3), m_pieceModels3); + else + m_pieceModels3.clear(); +} + +void GameModel::createPieceModels(Color c, QList& pieceModels) +{ + auto& bd = getBoard(); + auto nuPieces = bd.get_nu_uniq_pieces(); + pieceModels.clear(); + pieceModels.reserve(nuPieces); + for (Piece::IntType i = 0; i < nuPieces; ++i) + { + Piece piece(i); + for (unsigned j = 0; j < bd.get_piece_info(piece).get_nu_instances(); + ++j) + pieceModels.append(new PieceModel(this, bd, piece, c)); + } +} + +void GameModel::deleteAllVar() +{ + if (! is_main_variation(m_game.get_current())) + emit positionAboutToChange(); + m_game.delete_all_variations(); + updateProperties(); +} + +bool GameModel::findMove(const PieceModel& pieceModel, const QString& state, + QPointF coord, Move& mv) const +{ + auto piece = pieceModel.getPiece(); + auto& bd = getBoard(); + if (piece.to_int() >= bd.get_nu_uniq_pieces()) + { + qWarning("GameModel::findMove: pieceModel invalid in game variant"); + return false; + } + auto transform = pieceModel.getTransform(state); + if (! transform) + { + qWarning("GameModel::findMove: transform not found"); + return false; + } + auto& info = bd.get_piece_info(piece); + PiecePoints piecePoints = info.get_points(); + transform->transform(piecePoints.begin(), piecePoints.end()); + auto boardType = bd.get_board_type(); + auto newPointType = transform->get_new_point_type(); + bool pointTypeChanged = + ((boardType == BoardType::trigon && newPointType == 1) + || (boardType == BoardType::trigon_3 && newPointType == 0)); + QPointF center(PieceModel::findCenter(bd, piecePoints, false)); + // Round y of center to a multiple of 0.5, works better in Trigon + center.setY(round(2 * center.y()) / 2); + int offX = static_cast(round(coord.x() - center.x())); + int offY = static_cast(round(coord.y() - center.y())); + auto& geo = bd.get_geometry(); + MovePoints points; + for (auto& p : piecePoints) + { + int x = p.x + offX; + int y = p.y + offY; + if (! geo.is_onboard(CoordPoint(x, y))) + return false; + auto pointType = geo.get_point_type(p); + auto boardPointType = geo.get_point_type(x, y); + if (! pointTypeChanged && pointType != boardPointType) + return false; + if (pointTypeChanged && pointType == boardPointType) + return false; + points.push_back(geo.get_point(x, y)); + } + return bd.find_move(points, piece, mv); +} + +QString GameModel::getResultMessage() +{ + auto& bd = getBoard(); + auto nuPlayers = bd.get_nu_players(); + bool breakTies = (bd.get_piece_set() == PieceSet::callisto); + if (m_nuColors == 2) + { + auto score = m_points0 - m_points1; + if (score == 1) + return tr("Blue wins with 1 point."); + if (score > 0) + return tr("Blue wins with %1 points.").arg(score); + if (score == -1) + return tr("Green wins with 1 point."); + if (score < 0) + return tr("Green wins with %1 points.").arg(-score); + if (breakTies) + return tr("Green wins (tie resolved)."); + return tr("Game ends in a tie."); + } + if (m_nuColors == 4 && nuPlayers == 2) + { + auto score = m_points0 + m_points2 - m_points1 - m_points3; + if (score == 1) + return tr("Blue/Red wins with 1 point."); + if (score > 0) + return tr("Blue/Red wins with %1 points.").arg(score); + if (score == -1) + return tr("Yellow/Green wins with 1 point."); + if (score < 0) + return tr("Yellow/Green wins with %1 points.").arg(-score); + if (breakTies) + return tr("Yellow/Green wins (tie resolved)."); + return tr("Game ends in a tie."); + } + if (nuPlayers == 3) + { + auto maxPoints = max(max(m_points0, m_points1), m_points2); + unsigned nuWinners = 0; + if (m_points0 == maxPoints) + ++nuWinners; + if (m_points1 == maxPoints) + ++nuWinners; + if (m_points2 == maxPoints) + ++nuWinners; + if (m_points0 == maxPoints && nuWinners == 1) + return tr("Blue wins."); + if (m_points1 == maxPoints && nuWinners == 1) + return tr("Yellow wins."); + if (m_points2 == maxPoints && nuWinners == 1) + return tr("Red wins."); + if (m_points2 == maxPoints && breakTies) + return tr("Red wins (tie resolved)."); + if (m_points1 == maxPoints && breakTies) + return tr("Yellow wins (tie resolved)."); + if (m_points0 == maxPoints && m_points1 == maxPoints && nuWinners == 2) + return tr("Game ends in a tie between Blue and Yellow."); + if (m_points0 == maxPoints && m_points2 == maxPoints && nuWinners == 2) + return tr("Game ends in a tie between Blue and Red."); + if (nuWinners == 2) + return tr("Game ends in a tie between Yellow and Red."); + return tr("Game ends in a tie between all players."); + } + auto maxPoints = max(max(m_points0, m_points1), max(m_points2, m_points3)); + unsigned nuWinners = 0; + if (m_points0 == maxPoints) + ++nuWinners; + if (m_points1 == maxPoints) + ++nuWinners; + if (m_points2 == maxPoints) + ++nuWinners; + if (m_points3 == maxPoints) + ++nuWinners; + if (m_points0 == maxPoints && nuWinners == 1) + return tr("Blue wins."); + if (m_points1 == maxPoints && nuWinners == 1) + return tr("Yellow wins."); + if (m_points2 == maxPoints && nuWinners == 1) + return tr("Red wins."); + if (m_points3 == maxPoints && nuWinners == 1) + return tr("Green wins."); + if (m_points3 == maxPoints && breakTies) + return tr("Green wins (tie resolved)."); + if (m_points2 == maxPoints && breakTies) + return tr("Red wins (tie resolved)."); + if (m_points1 == maxPoints && breakTies) + return tr("Yellow wins (tie resolved)."); + if (m_points0 == maxPoints && m_points1 == maxPoints + && m_points2 == maxPoints && nuWinners == 3) + return tr("Game ends in a tie between Blue, Yellow and Red."); + if (m_points0 == maxPoints && m_points1 == maxPoints + && m_points3 == maxPoints && nuWinners == 3) + return tr("Game ends in a tie between Blue, Yellow and Green."); + if (m_points0 == maxPoints && m_points2 == maxPoints + && m_points3 == maxPoints && nuWinners == 3) + return tr("Game ends in a tie between Blue, Red and Green."); + if (nuWinners == 3) + return tr("Game ends in a tie between Yellow, Red and Green."); + if (m_points0 == maxPoints && m_points1 == maxPoints && nuWinners == 2) + return tr("Game ends in a tie between Blue and Yellow."); + if (m_points0 == maxPoints && m_points2 == maxPoints && nuWinners == 2) + return tr("Game ends in a tie between Blue and Red."); + if (nuWinners == 2) + return tr("Game ends in a tie between Yellow and Red."); + return tr("Game ends in a tie between all players."); +} + +Variant GameModel::getInitialGameVariant() +{ + QSettings settings; + auto variantString = settings.value("variant", "").toString(); + Variant variant; + if (! parse_variant_id(variantString.toLocal8Bit().constData(), variant)) + variant = Variant::duo; + return variant; +} + +QList& GameModel::getPieceModels(Color c) +{ + if (c == Color(0)) + return m_pieceModels0; + else if (c == Color(1)) + return m_pieceModels1; + else if (c == Color(2)) + return m_pieceModels2; + else + return m_pieceModels3; +} + +void GameModel::goBackward() +{ + gotoNode(m_game.get_current().get_parent_or_null()); +} + +void GameModel::goBeginning() +{ + gotoNode(m_game.get_root()); +} + +void GameModel::goEnd() +{ + gotoNode(get_last_node(m_game.get_current())); +} + +void GameModel::goForward() +{ + gotoNode(m_game.get_current().get_first_child_or_null()); +} + +void GameModel::goNextVar() +{ + gotoNode(m_game.get_current().get_sibling()); +} + +void GameModel::goPrevVar() +{ + gotoNode(m_game.get_current().get_previous_sibling()); +} + +void GameModel::gotoNode(const SgfNode& node) +{ + if (&node == &m_game.get_current()) + return; + emit positionAboutToChange(); + try + { + m_game.goto_node(node); + } + catch (const InvalidTree&) + { + } + updateProperties(); +} + +void GameModel::gotoNode(const SgfNode* node) +{ + if (node) + gotoNode(*node); +} + +void GameModel::initGameVariant(const QString& gameVariant) +{ + Variant variant; + if (! parse_variant_id(gameVariant.toLocal8Bit().constData(), variant)) + { + qWarning("GameModel: invalid game variant"); + return; + } + if (m_game.get_variant() != variant) + m_game.init(variant); + auto& bd = getBoard(); + set(m_nuColors, static_cast(bd.get_nu_colors()), + &GameModel::nuColorsChanged); + m_lastMovePieceModel = nullptr; + createPieceModels(); + m_gameVariant = gameVariant; + emit gameVariantChanged(gameVariant); + updateProperties(); +} + +bool GameModel::isLegalPos(PieceModel* pieceModel, const QString& state, + QPointF coord) const +{ + Move mv; + if (! findMove(*pieceModel, state, coord, mv)) + return false; + Color c(static_cast(pieceModel->color())); + bool result = getBoard().is_legal(c, mv); + return result; +} + +bool GameModel::loadAutoSave() +{ + QSettings settings; + auto s = settings.value("autosave", "").toByteArray(); + istringstream in(s.constData()); + if (! open(in)) + return false; + m_game.set_modified(); + return true; +} + +void GameModel::makeMainVar() +{ + m_game.make_main_variation(); + updateProperties(); +} + +void GameModel::moveDownVar() +{ + m_game.move_down_variation(); + updateProperties(); +} + +void GameModel::moveUpVar() +{ + m_game.move_up_variation(); + updateProperties(); +} + +void GameModel::nextColor() +{ + emit positionAboutToChange(); + auto& bd = getBoard(); + m_game.set_to_play(bd.get_next(bd.get_to_play())); + updateProperties(); +} + +void GameModel::newGame() +{ + emit positionAboutToChange(); + m_game.init(); + for (auto pieceModel : m_pieceModels0) + pieceModel->setDefaultState(); + for (auto pieceModel : m_pieceModels1) + pieceModel->setDefaultState(); + for (auto pieceModel : m_pieceModels2) + pieceModel->setDefaultState(); + for (auto pieceModel : m_pieceModels3) + pieceModel->setDefaultState(); + updateProperties(); +} + +bool GameModel::open(istream& in) +{ + try + { + TreeReader reader; + reader.read(in); + auto root = reader.get_tree_transfer_ownership(); + emit positionAboutToChange(); + m_game.init(root); + auto variant = to_string_id(m_game.get_variant()); + if (variant != m_gameVariant) + initGameVariant(variant); + goEnd(); + updateProperties(); + QSettings settings; + settings.remove("autosave"); + } + catch (const runtime_error& e) + { + m_lastInputOutputError = QString::fromLocal8Bit(e.what()); + return false; + } + return true; +} + +bool GameModel::open(const QString& file) +{ + ifstream in(file.toLocal8Bit().constData()); + if (! in) + { + m_lastInputOutputError = QString::fromLocal8Bit(strerror(errno)); + return false; + } + return open(in); +} + +QQmlListProperty GameModel::pieceModels0() +{ + return QQmlListProperty(this, m_pieceModels0); +} + +QQmlListProperty GameModel::pieceModels1() +{ + return QQmlListProperty(this, m_pieceModels1); +} + +QQmlListProperty GameModel::pieceModels2() +{ + return QQmlListProperty(this, m_pieceModels2); +} + +QQmlListProperty GameModel::pieceModels3() +{ + return QQmlListProperty(this, m_pieceModels3); +} + +void GameModel::playMove(int move) +{ + Move mv(static_cast(move)); + if (mv.is_null()) + return; + emit positionAboutToChange(); + m_game.play(m_game.get_to_play(), mv, false); + updateProperties(); +} + +void GameModel::playPiece(PieceModel* pieceModel, QPointF coord) +{ + Color c(static_cast(pieceModel->color())); + Move mv; + if (! findMove(*pieceModel, pieceModel->state(), coord, mv)) + { + qWarning("GameModel::play: illegal move"); + return; + } + emit positionAboutToChange(); + preparePieceGameCoord(pieceModel, mv); + pieceModel->setIsPlayed(true); + preparePieceTransform(pieceModel, mv); + m_game.play(c, mv, false); + updateProperties(); +} + +PieceModel* GameModel::preparePiece(int color, int move) +{ + Move mv(static_cast(move)); + Color c(static_cast(color)); + Piece piece = getBoard().get_move_piece(mv); + for (auto pieceModel : getPieceModels(c)) + if (pieceModel->getPiece() == piece && ! pieceModel->isPlayed()) + { + preparePieceTransform(pieceModel, mv); + preparePieceGameCoord(pieceModel, mv); + return pieceModel; + } + return nullptr; +} + +void GameModel::preparePieceGameCoord(PieceModel* pieceModel, Move mv) +{ + pieceModel->setGameCoord(getGameCoord(getBoard(), mv)); +} + +void GameModel::preparePieceTransform(PieceModel* pieceModel, Move mv) +{ + auto& bd = getBoard(); + auto transform = bd.find_transform(mv); + auto& pieceInfo = bd.get_piece_info(bd.get_move_piece(mv)); + if (! compareTransform(pieceInfo, pieceModel->getTransform(), transform)) + pieceModel->setTransform(transform); +} + +bool GameModel::save(const QString& file) +{ + ofstream out(file.toLocal8Bit().constData()); + PentobiTreeWriter writer(out, m_game.get_tree()); + writer.set_indent(1); + writer.write(); + if (! out) + { + m_lastInputOutputError = QString::fromLocal8Bit(strerror(errno)); + return false; + } + m_game.clear_modified(); + return true; +} + +template +void GameModel::set(T& target, const T& value, + void (GameModel::*changedSignal)(T)) +{ + if (target != value) + { + target = value; + emit (this->*changedSignal)(value); + } +} + +void GameModel::truncate() +{ + if (! m_game.get_current().has_parent()) + return; + emit positionAboutToChange(); + m_game.truncate(); + updateProperties(); +} + +void GameModel::truncateChildren() +{ + m_game.truncate_children(); + updateProperties(); +} + +void GameModel::undo() +{ + if (! m_canUndo) + return; + emit positionAboutToChange(); + m_game.undo(); + updateProperties(); +} + +/** Helper function for updateProperties() */ +PieceModel* GameModel::updatePiece(Color c, Move mv, + array& isPlayed) +{ + auto& bd = getBoard(); + Piece piece = bd.get_move_piece(mv); + auto& pieceInfo = bd.get_piece_info(piece); + auto gameCoord = getGameCoord(bd, mv); + auto transform = bd.find_transform(mv); + auto& pieceModels = getPieceModels(c); + // Prefer piece models already played with the given gameCoord and + // transform because class Board doesn't make a distinction between + // instances of the same piece (in Junior) and we want to avoid + // unwanted piece movement animations to switch instances. + for (int i = 0; i < pieceModels.length(); ++i) + if (pieceModels[i]->getPiece() == piece + && pieceModels[i]->isPlayed() + && compareGameCoord(pieceModels[i]->gameCoord(), gameCoord) + && compareTransform(pieceInfo, pieceModels[i]->getTransform(), + transform)) + { + isPlayed[i] = true; + return pieceModels[i]; + } + for (int i = 0; i < pieceModels.length(); ++i) + if (pieceModels[i]->getPiece() == piece && ! isPlayed[i]) + { + isPlayed[i] = true; + // Order is important: isPlayed will trigger an animation to move + // the piece, so it needs to be set after gameCoord. + pieceModels[i]->setGameCoord(gameCoord); + pieceModels[i]->setIsPlayed(true); + pieceModels[i]->setTransform(transform); + return pieceModels[i]; + } + LIBBOARDGAME_ASSERT(false); + return nullptr; +} + +void GameModel::updateProperties() +{ + auto& bd = getBoard(); + auto& geo = bd.get_geometry(); + auto& tree = m_game.get_tree(); + bool isTrigon = (bd.get_piece_set() == PieceSet::trigon); + set(m_points0, bd.get_points(Color(0)), &GameModel::points0Changed); + set(m_points1, bd.get_points(Color(1)), &GameModel::points1Changed); + set(m_bonus0, bd.get_bonus(Color(0)), &GameModel::bonus0Changed); + set(m_bonus1, bd.get_bonus(Color(1)), &GameModel::bonus1Changed); + set(m_hasMoves0, bd.has_moves(Color(0)), &GameModel::hasMoves0Changed); + set(m_hasMoves1, bd.has_moves(Color(1)), &GameModel::hasMoves1Changed); + bool isFirstPieceAny = false; + if (m_nuColors > 2) + { + set(m_points2, bd.get_points(Color(2)), &GameModel::points2Changed); + set(m_bonus2, bd.get_bonus(Color(2)), &GameModel::bonus2Changed); + set(m_hasMoves2, bd.has_moves(Color(2)), &GameModel::hasMoves2Changed); + } + if (m_nuColors > 3) + { + set(m_points3, bd.get_points(Color(3)), &GameModel::points3Changed); + set(m_bonus3, bd.get_bonus(Color(3)), &GameModel::bonus3Changed); + set(m_hasMoves3, bd.has_moves(Color(3)), &GameModel::hasMoves3Changed); + } + m_tmpPoints.clear(); + if (bd.is_first_piece(Color(0))) + { + isFirstPieceAny = true; + if (! isTrigon) + for (Point p : bd.get_starting_points(Color(0))) + m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p))); + } + set(m_startingPoints0, m_tmpPoints, &GameModel::startingPoints0Changed); + m_tmpPoints.clear(); + if (bd.is_first_piece(Color(1))) + { + isFirstPieceAny = true; + if (! isTrigon) + for (Point p : bd.get_starting_points(Color(1))) + m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p))); + } + set(m_startingPoints1, m_tmpPoints, &GameModel::startingPoints1Changed); + m_tmpPoints.clear(); + if (m_nuColors > 2 && bd.is_first_piece(Color(2))) + { + isFirstPieceAny = true; + if (! isTrigon) + for (Point p : bd.get_starting_points(Color(2))) + m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p))); + } + set(m_startingPoints2, m_tmpPoints, &GameModel::startingPoints2Changed); + m_tmpPoints.clear(); + if (m_nuColors > 3 && bd.is_first_piece(Color(3))) + { + isFirstPieceAny = true; + if (! isTrigon) + for (Point p : bd.get_starting_points(Color(3))) + m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p))); + } + set(m_startingPoints3, m_tmpPoints, &GameModel::startingPoints3Changed); + m_tmpPoints.clear(); + if (isTrigon && isFirstPieceAny) + for (Point p : bd.get_starting_points(Color(0))) + m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p))); + set(m_startingPointsAll, m_tmpPoints, + &GameModel::startingPointsAllChanged); + auto& current = m_game.get_current(); + set(m_canUndo, + ! current.has_children() && tree.has_move_ignore_invalid(current) + && current.has_parent(), + &GameModel::canUndoChanged); + set(m_canGoForward, current.has_children(), + &GameModel::canGoForwardChanged); + set(m_canGoBackward, current.has_parent(), + &GameModel::canGoBackwardChanged); + set(m_hasPrevVar, (current.get_previous_sibling() != nullptr), + &GameModel::hasPrevVarChanged); + set(m_hasNextVar, (current.get_sibling() != nullptr), + &GameModel::hasNextVarChanged); + set(m_hasVariations, tree.has_variations(), + &GameModel::hasVariationsChanged); + set(m_isMainVar, is_main_variation(current), + &GameModel::isMainVarChanged); + auto positionInfo + = QString::fromLocal8Bit(get_position_info(tree, current).c_str()); + if (positionInfo.isEmpty()) + positionInfo = bd.has_setup() ? tr("(Setup)") : tr("(No moves)"); + else + { + positionInfo = tr("Move %1").arg(positionInfo); + if (bd.get_nu_moves() == 0 && bd.has_setup()) + { + positionInfo.append(' '); + positionInfo.append(tr("(Setup)")); + } + } + set(m_positionInfo, positionInfo, &GameModel::positionInfoChanged); + bool isGameOver = true; + for (Color c : bd.get_colors()) + if (bd.has_moves(c)) + { + isGameOver = false; + break; + } + set(m_isGameOver, isGameOver, &GameModel::isGameOverChanged); + set(m_isGameEmpty, libboardgame_sgf::util::is_empty(tree), + &GameModel::isGameEmptyChanged); + + ColorMap> isPlayed; + for (Color c : bd.get_colors()) + { + isPlayed[c].fill(false); + for (Move mv : bd.get_setup().placements[c]) + updatePiece(c, mv, isPlayed[c]); + } + PieceModel* lastMovePieceModel = nullptr; + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + auto mv = bd.get_move(i); + auto c = mv.color; + lastMovePieceModel = updatePiece(c, mv.move, isPlayed[c]); + } + if (lastMovePieceModel != m_lastMovePieceModel) + { + if (m_lastMovePieceModel != nullptr) + m_lastMovePieceModel->setIsLastMove(false); + if (lastMovePieceModel != nullptr) + lastMovePieceModel->setIsLastMove(true); + m_lastMovePieceModel = lastMovePieceModel; + } + for (Color c : bd.get_colors()) + { + auto& pieceModels = getPieceModels(c); + for (int i = 0; i < pieceModels.length(); ++i) + if (! isPlayed[c][i] && pieceModels[i]->isPlayed()) + { + pieceModels[i]->setDefaultState(); + pieceModels[i]->setIsPlayed(false); + } + } + + set(m_toPlay, m_isGameOver ? 0 : bd.get_effective_to_play().to_int(), + &GameModel::toPlayChanged); + set(m_altPlayer, + bd.get_variant() == Variant::classic_3 ? bd.get_alt_player() : 0, + &GameModel::altPlayerChanged); + + emit positionChanged(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_qml/GameModel.h b/src/pentobi_qml/GameModel.h new file mode 100644 index 0000000..f192b40 --- /dev/null +++ b/src/pentobi_qml/GameModel.h @@ -0,0 +1,325 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/GameModel.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_QML_GAME_MODEL_H +#define PENTOBI_QML_GAME_MODEL_H + +#include +#include "PieceModel.h" +#include "libpentobi_base/Game.h" + +using namespace std; +using libboardgame_sgf::SgfNode; +using libpentobi_base::Board; +using libpentobi_base::Game; +using libpentobi_base::Move; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class GameModel + : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString gameVariant MEMBER m_gameVariant + NOTIFY gameVariantChanged) + Q_PROPERTY(QString positionInfo MEMBER m_positionInfo + NOTIFY positionInfoChanged) + Q_PROPERTY(QString lastInputOutputError MEMBER m_lastInputOutputError) + Q_PROPERTY(int nuColors MEMBER m_nuColors NOTIFY nuColorsChanged) + Q_PROPERTY(int toPlay MEMBER m_toPlay NOTIFY toPlayChanged) + Q_PROPERTY(int altPlayer MEMBER m_altPlayer NOTIFY altPlayerChanged) + Q_PROPERTY(float points0 MEMBER m_points0 NOTIFY points0Changed) + Q_PROPERTY(float points1 MEMBER m_points1 NOTIFY points1Changed) + Q_PROPERTY(float points2 MEMBER m_points2 NOTIFY points2Changed) + Q_PROPERTY(float points3 MEMBER m_points3 NOTIFY points3Changed) + Q_PROPERTY(float bonus0 MEMBER m_bonus0 NOTIFY bonus0Changed) + Q_PROPERTY(float bonus1 MEMBER m_bonus1 NOTIFY bonus1Changed) + Q_PROPERTY(float bonus2 MEMBER m_bonus2 NOTIFY bonus2Changed) + Q_PROPERTY(float bonus3 MEMBER m_bonus3 NOTIFY bonus3Changed) + Q_PROPERTY(bool hasMoves0 MEMBER m_hasMoves0 NOTIFY hasMoves0Changed) + Q_PROPERTY(bool hasMoves1 MEMBER m_hasMoves1 NOTIFY hasMoves1Changed) + Q_PROPERTY(bool hasMoves2 MEMBER m_hasMoves2 NOTIFY hasMoves2Changed) + Q_PROPERTY(bool hasMoves3 MEMBER m_hasMoves3 NOTIFY hasMoves3Changed) + Q_PROPERTY(bool isGameOver MEMBER m_isGameOver NOTIFY isGameOverChanged) + Q_PROPERTY(bool isGameEmpty MEMBER m_isGameEmpty NOTIFY isGameEmptyChanged) + Q_PROPERTY(bool canUndo MEMBER m_canUndo NOTIFY canUndoChanged) + Q_PROPERTY(bool canGoBackward MEMBER m_canGoBackward + NOTIFY canGoBackwardChanged) + Q_PROPERTY(bool canGoForward MEMBER m_canGoForward + NOTIFY canGoForwardChanged) + Q_PROPERTY(bool hasPrevVar MEMBER m_hasPrevVar NOTIFY hasPrevVarChanged) + Q_PROPERTY(bool hasNextVar MEMBER m_hasNextVar NOTIFY hasNextVarChanged) + Q_PROPERTY(bool hasVariations MEMBER m_hasVariations + NOTIFY hasVariationsChanged) + Q_PROPERTY(bool isMainVar MEMBER m_isMainVar NOTIFY isMainVarChanged) + Q_PROPERTY(QQmlListProperty pieceModels0 READ pieceModels0) + Q_PROPERTY(QQmlListProperty pieceModels1 READ pieceModels1) + Q_PROPERTY(QQmlListProperty pieceModels2 READ pieceModels2) + Q_PROPERTY(QQmlListProperty pieceModels3 READ pieceModels3) + Q_PROPERTY(QVariantList startingPoints0 MEMBER m_startingPoints0 + NOTIFY startingPoints0Changed) + Q_PROPERTY(QVariantList startingPoints1 MEMBER m_startingPoints1 + NOTIFY startingPoints1Changed) + Q_PROPERTY(QVariantList startingPoints2 MEMBER m_startingPoints2 + NOTIFY startingPoints2Changed) + Q_PROPERTY(QVariantList startingPoints3 MEMBER m_startingPoints3 + NOTIFY startingPoints3Changed) + Q_PROPERTY(QVariantList startingPointsAll MEMBER m_startingPointsAll + NOTIFY startingPointsAllChanged) + +public: + static Variant getInitialGameVariant(); + + explicit GameModel(QObject* parent = nullptr); + + Q_INVOKABLE void deleteAllVar(); + + Q_INVOKABLE bool isLegalPos(PieceModel* pieceModel, const QString& state, + QPointF coord) const; + + Q_INVOKABLE void nextColor(); + + Q_INVOKABLE void playPiece(PieceModel* pieceModel, QPointF coord); + + Q_INVOKABLE void playMove(int move); + + Q_INVOKABLE void newGame(); + + Q_INVOKABLE void undo(); + + Q_INVOKABLE void goBeginning(); + + Q_INVOKABLE void goBackward(); + + Q_INVOKABLE void goForward(); + + Q_INVOKABLE void goEnd(); + + Q_INVOKABLE void goNextVar(); + + Q_INVOKABLE void goPrevVar(); + + Q_INVOKABLE void backToMainVar(); + + Q_INVOKABLE void initGameVariant(const QString& gameVariant); + + Q_INVOKABLE void autoSave(); + + Q_INVOKABLE bool loadAutoSave(); + + /** Find the piece model for a given move and set its transform and game + coordinates accordingly but do not set its status to played yet. */ + Q_INVOKABLE PieceModel* preparePiece(int color, int move); + + Q_INVOKABLE bool save(const QString& file); + + Q_INVOKABLE bool open(const QString& file); + + Q_INVOKABLE void makeMainVar(); + + Q_INVOKABLE void moveDownVar(); + + Q_INVOKABLE void moveUpVar(); + + Q_INVOKABLE void truncate(); + + Q_INVOKABLE void truncateChildren(); + + Q_INVOKABLE QString getResultMessage(); + + QQmlListProperty pieceModels0(); + + QQmlListProperty pieceModels1(); + + QQmlListProperty pieceModels2(); + + QQmlListProperty pieceModels3(); + + const Board& getBoard() const { return m_game.get_board(); } + +signals: + /** Position is about to change due to new game or navigation or editing of + the game tree. */ + void positionAboutToChange(); + + /** Position changed due to new game or navigation or editing of the + game tree. */ + void positionChanged(); + + void toPlayChanged(int); + + void altPlayerChanged(int); + + void points0Changed(float); + + void points1Changed(float); + + void points2Changed(float); + + void points3Changed(float); + + void bonus0Changed(float); + + void bonus1Changed(float); + + void bonus2Changed(float); + + void bonus3Changed(float); + + void hasMoves0Changed(bool); + + void hasMoves1Changed(bool); + + void hasMoves2Changed(bool); + + void hasMoves3Changed(bool); + + void hasVariationsChanged(bool); + + void isGameOverChanged(bool); + + void isGameEmptyChanged(bool); + + void isMainVarChanged(bool); + + void canUndoChanged(bool); + + void canGoBackwardChanged(bool); + + void canGoForwardChanged(bool); + + void hasPrevVarChanged(bool); + + void hasNextVarChanged(bool); + + void gameVariantChanged(QString); + + void positionInfoChanged(QString); + + void nuColorsChanged(int); + + void startingPoints0Changed(QVariantList); + + void startingPoints1Changed(QVariantList); + + void startingPoints2Changed(QVariantList); + + void startingPoints3Changed(QVariantList); + + void startingPointsAllChanged(QVariantList); + +private: + Game m_game; + + QString m_gameVariant; + + QString m_positionInfo; + + QString m_lastInputOutputError; + + int m_nuColors; + + int m_toPlay = 0; + + int m_altPlayer = 0; + + float m_points0 = 0; + + float m_points1 = 0; + + float m_points2 = 0; + + float m_points3 = 0; + + float m_bonus0 = 0; + + float m_bonus1 = 0; + + float m_bonus2 = 0; + + float m_bonus3 = 0; + + bool m_hasMoves0 = true; + + bool m_hasMoves1 = true; + + bool m_hasMoves2 = true; + + bool m_hasMoves3 = true; + + bool m_hasVariations = false; + + bool m_isGameOver = false; + + bool m_isGameEmpty = true; + + bool m_canUndo = false; + + bool m_canGoForward = false; + + bool m_canGoBackward = false; + + bool m_hasPrevVar = false; + + bool m_hasNextVar = false; + + bool m_isMainVar = true; + + QList m_pieceModels0; + + QList m_pieceModels1; + + QList m_pieceModels2; + + QList m_pieceModels3; + + PieceModel* m_lastMovePieceModel = nullptr; + + QVariantList m_startingPoints0; + + QVariantList m_startingPoints1; + + QVariantList m_startingPoints2; + + QVariantList m_startingPoints3; + + QVariantList m_startingPointsAll; + + QVariantList m_tmpPoints; + + + void createPieceModels(); + + void createPieceModels(Color c, QList& pieceModels); + + bool findMove(const PieceModel& pieceModel, const QString& state, + QPointF coord, Move& mv) const; + + QList& getPieceModels(Color c); + + void gotoNode(const SgfNode& node); + + void gotoNode(const SgfNode* node); + + bool open(istream& in); + + void preparePieceGameCoord(PieceModel* pieceModel, Move mv); + + void preparePieceTransform(PieceModel* pieceModel, Move mv); + + template + void set(T& target, const T& value, void (GameModel::*changedSignal)(T)); + + PieceModel* updatePiece(Color c, Move mv, + array& isPlayed); + + void updateProperties(); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_QML_GAME_MODEL_H diff --git a/src/pentobi_qml/Main.cpp b/src/pentobi_qml/Main.cpp new file mode 100644 index 0000000..3153f11 --- /dev/null +++ b/src/pentobi_qml/Main.cpp @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include "GameModel.h" +#include "PlayerModel.h" +#include "libboardgame_util/Log.h" + +using libboardgame_util::RandomGenerator; + +//----------------------------------------------------------------------------- + +int main(int argc, char *argv[]) +{ + libboardgame_util::LogInitializer log_initializer; + QApplication app(argc, argv); + app.setOrganizationName("Pentobi"); + app.setApplicationName("Pentobi"); +#ifdef VERSION + app.setApplicationVersion(VERSION); +#endif + qmlRegisterType("pentobi", 1, 0, "GameModel"); + qmlRegisterType("pentobi", 1, 0, "PlayerModel"); + qmlRegisterInterface("PieceModel"); + QString locale = QLocale::system().name(); + QTranslator translatorPentobi; + translatorPentobi.load("qml_" + locale, ":qml/i18n"); + app.installTranslator(&translatorPentobi); + // The translation of standard buttons in QtQuick.Dialogs.MessageDialog + // is broken on Android (tested with Qt 5.5; QTBUG-43353), so we + // created our own file, which contains the translations we need. + QTranslator translatorQt; + translatorQt.load("replace_qtbase_" + locale, ":qml/i18n"); + app.installTranslator(&translatorQt); + QCommandLineParser parser; + QCommandLineOption optionNoBook("nobook"); + parser.addOption(optionNoBook); + QCommandLineOption optionNoDelay("nodelay"); + parser.addOption(optionNoDelay); + QCommandLineOption optionSeed("seed", "Set random seed to .", "n"); + parser.addOption(optionSeed); + QCommandLineOption optionThreads("threads", "Use threads (0=auto).", + "n"); + parser.addOption(optionThreads); + QCommandLineOption optionVerbose("verbose"); + parser.addOption(optionVerbose); + parser.process(app); + try + { +#if LIBBOARDGAME_DISABLE_LOG + if (parser.isSet(optionVerbose)) + throw runtime_error("This version of Pentobi was compiled" + " without support for logging."); +#else + if (! parser.isSet(optionVerbose)) + libboardgame_util::disable_logging(); +#endif + if (parser.isSet(optionNoBook)) + PlayerModel::noBook = true; + if (parser.isSet(optionNoDelay)) + PlayerModel::noDelay = true; + bool ok; + if (parser.isSet(optionSeed)) + { + auto seed = parser.value(optionSeed).toUInt(&ok); + if (! ok) + throw runtime_error("--seed must be a positive number"); + RandomGenerator::set_global_seed(seed); + } + if (parser.isSet(optionThreads)) + { + auto nuThreads = parser.value(optionThreads).toUInt(&ok); + if (! ok) + throw runtime_error("--threads must be a positive number"); + PlayerModel::nuThreads = nuThreads; + } + QQmlApplicationEngine engine(QUrl("qrc:///qml/Main.qml")); + return app.exec(); + } + catch (const bad_alloc&) + { + // bad_alloc is an expected error because the player requires a larger + // amount of memory. + QMessageBox::critical(nullptr, app.translate("main", "Pentobi"), + app.translate("main", "Not enough memory.")); + return 1; + } + catch (const exception& e) + { + cerr << "Error: " << e.what() << '\n'; + return 1; + } +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_qml/Pentobi.pro b/src/pentobi_qml/Pentobi.pro new file mode 100644 index 0000000..b4ba11f --- /dev/null +++ b/src/pentobi_qml/Pentobi.pro @@ -0,0 +1,225 @@ +TEMPLATE = app + +QT += qml quick svg concurrent + +INCLUDEPATH += .. +CONFIG += c++11 +QMAKE_CXXFLAGS += -DVERSION=\"\\\"12.2\\\"\" +QMAKE_CXXFLAGS += -DPENTOBI_LOW_RESOURCES +android { + QMAKE_CXXFLAGS_RELEASE += -DLIBBOARDGAME_DISABLE_LOG +} +QMAKE_CXXFLAGS_DEBUG += -DLIBBOARDGAME_DEBUG +gcc { + QMAKE_CXXFLAGS_RELEASE -= -O + QMAKE_CXXFLAGS_RELEASE -= -O1 + QMAKE_CXXFLAGS_RELEASE -= -O2 + QMAKE_CXXFLAGS_RELEASE -= -O3 + QMAKE_CXXFLAGS_RELEASE -= -Os + QMAKE_CXXFLAGS_RELEASE *= -Ofast +} + +SOURCES += \ + GameModel.cpp \ + Main.cpp \ + PieceModel.cpp \ + PlayerModel.cpp \ + ../libboardgame_base/CoordPoint.cpp \ + ../libboardgame_base/Rating.cpp \ + ../libboardgame_base/RectTransform.cpp \ + ../libboardgame_base/StringRep.cpp \ + ../libboardgame_base/Transform.cpp \ + ../libboardgame_util/Abort.cpp \ + ../libboardgame_util/Assert.cpp \ + ../libboardgame_util/Barrier.cpp \ + ../libboardgame_util/CpuTimeSource.cpp \ + ../libboardgame_util/IntervalChecker.cpp \ + ../libboardgame_util/Log.cpp \ + ../libboardgame_util/RandomGenerator.cpp \ + ../libboardgame_util/StringUtil.cpp \ + ../libboardgame_util/TimeIntervalChecker.cpp \ + ../libboardgame_util/Timer.cpp \ + ../libboardgame_util/TimeSource.cpp \ + ../libboardgame_util/WallTimeSource.cpp \ + ../libboardgame_sgf/MissingProperty.cpp \ + ../libboardgame_sgf/Reader.cpp \ + ../libboardgame_sgf/SgfNode.cpp \ + ../libboardgame_sgf/SgfTree.cpp \ + ../libboardgame_sgf/SgfUtil.cpp \ + ../libboardgame_sgf/TreeReader.cpp \ + ../libboardgame_sgf/TreeWriter.cpp \ + ../libboardgame_sgf/Writer.cpp \ + ../libboardgame_sys/CpuTime.cpp \ + ../libboardgame_sys/Memory.cpp \ + ../libpentobi_base/Board.cpp \ + ../libpentobi_base/BoardConst.cpp \ + ../libpentobi_base/BoardUpdater.cpp \ + ../libpentobi_base/BoardUtil.cpp \ + ../libpentobi_base/Book.cpp \ + ../libpentobi_base/CallistoGeometry.cpp \ + ../libpentobi_base/Color.cpp \ + ../libpentobi_base/Game.cpp \ + ../libpentobi_base/NexosGeometry.cpp \ + ../libpentobi_base/NodeUtil.cpp \ + ../libpentobi_base/PentobiSgfUtil.cpp \ + ../libpentobi_base/PentobiTreeWriter.cpp \ + ../libpentobi_base/PieceInfo.cpp \ + ../libpentobi_base/PieceTransforms.cpp \ + ../libpentobi_base/PieceTransformsClassic.cpp \ + ../libpentobi_base/PieceTransformsTrigon.cpp \ + ../libpentobi_base/PointState.cpp \ + ../libpentobi_base/StartingPoints.cpp \ + ../libpentobi_base/SymmetricPoints.cpp \ + ../libpentobi_base/TreeUtil.cpp \ + ../libpentobi_base/TrigonGeometry.cpp \ + ../libpentobi_base/TrigonTransform.cpp \ + ../libpentobi_base/Variant.cpp \ + ../libpentobi_base/PlayerBase.cpp \ + ../libpentobi_base/PentobiTree.cpp \ + ../libpentobi_mcts/History.cpp \ + ../libpentobi_mcts/Player.cpp \ + ../libpentobi_mcts/PlayoutFeatures.cpp \ + ../libpentobi_mcts/PriorKnowledge.cpp \ + ../libpentobi_mcts/Search.cpp \ + ../libpentobi_mcts/SharedConst.cpp \ + ../libpentobi_mcts/State.cpp \ + ../libpentobi_mcts/Util.cpp \ + ../libpentobi_mcts/StateUtil.cpp + +RESOURCES += \ + ../books/pentobi_books.qrc \ + qml/themes/theme_shared.qrc \ + resources.qrc \ + translations.qrc + +android { + RESOURCES += \ + icons_android.qrc \ + qml/themes/theme_dark.qrc +} else { + RESOURCES += \ + ../pentobi/icons.qrc \ + qml/themes/theme_light.qrc +} + +# Default rules for deployment. +include(deployment.pri) + +HEADERS += \ + GameModel.h \ + PieceModel.h \ + PlayerModel.h \ + ../libboardgame_base/CoordPoint.h \ + ../libboardgame_base/Geometry.h \ + ../libboardgame_base/GeometryUtil.h \ + ../libboardgame_base/Grid.h \ + ../libboardgame_base/Marker.h \ + ../libboardgame_base/Point.h \ + ../libboardgame_base/PointTransform.h \ + ../libboardgame_base/Rating.h \ + ../libboardgame_base/RectGeometry.h \ + ../libboardgame_base/RectTransform.h \ + ../libboardgame_base/StringRep.h \ + ../libboardgame_base/Transform.h \ + ../libboardgame_mcts/Atomic.h \ + ../libboardgame_mcts/LastGoodReply.h \ + ../libboardgame_mcts/Node.h \ + ../libboardgame_mcts/PlayerMove.h \ + ../libboardgame_mcts/SearchBase.h \ + ../libboardgame_mcts/Tree.h \ + ../libboardgame_mcts/TreeUtil.h \ + ../libboardgame_util/Abort.h \ + ../libboardgame_util/ArrayList.h \ + ../libboardgame_util/Assert.h \ + ../libboardgame_util/Barrier.h \ + ../libboardgame_util/CpuTimeSource.h \ + ../libboardgame_util/FmtSaver.h \ + ../libboardgame_util/IntervalChecker.h \ + ../libboardgame_util/Log.h \ + ../libboardgame_util/MathUtil.h \ + ../libboardgame_util/Options.h \ + ../libboardgame_util/RandomGenerator.h \ + ../libboardgame_util/Statistics.h \ + ../libboardgame_util/StringUtil.h \ + ../libboardgame_util/TimeIntervalChecker.h \ + ../libboardgame_util/Timer.h \ + ../libboardgame_util/TimeSource.h \ + ../libboardgame_util/Unused.h \ + ../libboardgame_util/WallTimeSource.h \ + ../libboardgame_sgf/InvalidPropertyValue.h \ + ../libboardgame_sgf/InvalidTree.h \ + ../libboardgame_sgf/MissingProperty.h \ + ../libboardgame_sgf/Reader.h \ + ../libboardgame_sgf/SgfNode.h \ + ../libboardgame_sgf/SgfTree.h \ + ../libboardgame_sgf/SgfUtil.h \ + ../libboardgame_sgf/TreeReader.h \ + ../libboardgame_sgf/Writer.h \ + ../libboardgame_sys/Compiler.h \ + ../libboardgame_sys/CpuTime.h \ + ../libboardgame_sys/Memory.h \ + ../libpentobi_base/Board.h \ + ../libpentobi_base/BoardConst.h \ + ../libpentobi_base/BoardUpdater.h \ + ../libpentobi_base/BoardUtil.h \ + ../libpentobi_base/Book.h \ + ../libpentobi_base/Color.h \ + ../libpentobi_base/ColorMap.h \ + ../libpentobi_base/ColorMove.h \ + ../libpentobi_base/Game.h \ + ../libpentobi_base/Geometry.h \ + ../libpentobi_base/Grid.h \ + ../libpentobi_base/Marker.h \ + ../libpentobi_base/Move.h \ + ../libpentobi_base/MoveInfo.h \ + ../libpentobi_base/MoveList.h \ + ../libpentobi_base/MoveMarker.h \ + ../libpentobi_base/MovePoints.h \ + ../libpentobi_base/NexosGeometry.h \ + ../libpentobi_base/NodeUtil.h \ + ../libpentobi_base/PentobiTree.h \ + ../libpentobi_base/Piece.h \ + ../libpentobi_base/PieceInfo.h \ + ../libpentobi_base/PieceMap.h \ + ../libpentobi_base/PieceTransforms.h \ + ../libpentobi_base/PieceTransformsClassic.h \ + ../libpentobi_base/PieceTransformsTrigon.h \ + ../libpentobi_base/PlayerBase.h \ + ../libpentobi_base/Point.h \ + ../libpentobi_base/PointList.h \ + ../libpentobi_base/PointState.h \ + ../libpentobi_base/PrecompMoves.h \ + ../libpentobi_base/Setup.h \ + ../libpentobi_base/PentobiSgfUtil.h \ + ../libpentobi_base/StartingPoints.h \ + ../libpentobi_base/SymmetricPoints.h \ + ../libpentobi_base/TreeUtil.h \ + ../libpentobi_base/TrigonGeometry.h \ + ../libpentobi_base/TrigonTransform.h \ + ../libpentobi_base/Variant.h \ + ../libpentobi_mcts/Float.h \ + ../libpentobi_mcts/History.h \ + ../libpentobi_mcts/Player.h \ + ../libpentobi_mcts/PlayoutFeatures.h \ + ../libpentobi_mcts/PriorKnowledge.h \ + ../libpentobi_mcts/Search.h \ + ../libpentobi_mcts/SearchParamConst.h \ + ../libpentobi_mcts/SharedConst.h \ + ../libpentobi_mcts/State.h \ + ../libpentobi_mcts/StateUtil.h \ + ../libpentobi_mcts/Util.h + +lupdate_only { +SOURCES += \ + qml/*.qml \ + qml/*.js +} + +TRANSLATIONS += \ + qml/i18n/qml_de.ts \ + qml/i18n/replace_qtbase_de.ts + +OTHER_FILES += \ + android/AndroidManifest.xml + +ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android diff --git a/src/pentobi_qml/PieceModel.cpp b/src/pentobi_qml/PieceModel.cpp new file mode 100644 index 0000000..8c255b0 --- /dev/null +++ b/src/pentobi_qml/PieceModel.cpp @@ -0,0 +1,373 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/PieceModel.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "PieceModel.h" + +#include +#include "libboardgame_base/RectTransform.h" +#include "libpentobi_base/TrigonTransform.h" + +using namespace std; +using libboardgame_base::ArrayList; +using libboardgame_base::CoordPoint; +using libboardgame_base::TransfIdentity; +using libboardgame_base::TransfRectRot90; +using libboardgame_base::TransfRectRot180; +using libboardgame_base::TransfRectRot270; +using libboardgame_base::TransfRectRefl; +using libboardgame_base::TransfRectRot90Refl; +using libboardgame_base::TransfRectRot180Refl; +using libboardgame_base::TransfRectRot270Refl; +using libpentobi_base::BoardType; +using libpentobi_base::PieceInfo; +using libpentobi_base::PieceSet; +using libpentobi_base::TransfTrigonIdentity; +using libpentobi_base::TransfTrigonRefl; +using libpentobi_base::TransfTrigonReflRot60; +using libpentobi_base::TransfTrigonReflRot120; +using libpentobi_base::TransfTrigonReflRot180; +using libpentobi_base::TransfTrigonReflRot240; +using libpentobi_base::TransfTrigonReflRot300; +using libpentobi_base::TransfTrigonRot60; +using libpentobi_base::TransfTrigonRot120; +using libpentobi_base::TransfTrigonRot180; +using libpentobi_base::TransfTrigonRot240; +using libpentobi_base::TransfTrigonRot300; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +PieceModel::PieceModel(QObject* parent, const Board& bd, Piece piece, Color c) + : QObject(parent), + m_bd(bd), + m_color(c), + m_piece(piece) +{ + auto& geo = bd.get_geometry(); + bool isNexos = (bd.get_piece_set() == PieceSet::nexos); + bool isCallisto = (bd.get_piece_set() == PieceSet::callisto); + auto& info = bd.get_piece_info(piece); + auto& points = info.get_points(); + m_elements.reserve(points.size()); + for (auto& p : points) + { + if (isNexos && geo.get_point_type(p) == 0) + continue; + m_elements.append(QPointF(p.x, p.y)); + } + if (isNexos) + { + ArrayList candidates; + for (auto& p : points) + { + auto pointType = geo.get_point_type(p); + if (pointType == 1) + { + candidates.include(CoordPoint(p.x - 1, p. y)); + candidates.include(CoordPoint(p.x + 1, p. y)); + } + else if (pointType == 2) + { + candidates.include(CoordPoint(p.x, p. y - 1)); + candidates.include(CoordPoint(p.x, p. y + 1)); + } + } + m_junctions.reserve(candidates.size()); + m_junctionType.reserve(candidates.size()); + for (auto& p : candidates) + { + bool hasLeft = points.contains(CoordPoint(p.x - 1, p. y)); + bool hasRight = points.contains(CoordPoint(p.x + 1, p. y)); + bool hasUp = points.contains(CoordPoint(p.x, p. y - 1)); + bool hasDown = points.contains(CoordPoint(p.x, p. y + 1)); + int junctionType; + if (hasLeft && hasRight && hasUp && hasDown) + junctionType = 0; + else if (hasRight && hasUp && hasDown) + junctionType = 1; + else if (hasLeft && hasUp && hasDown) + junctionType = 2; + else if (hasLeft && hasRight && hasDown) + junctionType = 3; + else if (hasLeft && hasRight && hasUp) + junctionType = 4; + else if (hasLeft && hasRight) + junctionType = 5; + else if (hasUp && hasDown) + junctionType = 6; + else if (hasLeft && hasUp) + junctionType = 7; + else if (hasLeft && hasDown) + junctionType = 8; + else if (hasRight && hasUp) + junctionType = 9; + else if (hasRight && hasDown) + junctionType = 10; + else + continue; + m_junctions.append(QPointF(p.x, p.y)); + m_junctionType.append(junctionType); + } + } + if (isCallisto) + for (auto& p : points) + { + bool hasRight = points.contains(CoordPoint(p.x + 1, p. y)); + bool hasDown = points.contains(CoordPoint(p.x, p. y + 1)); + int junctionType; + if (hasRight && hasDown) + junctionType = 0; + else if (hasRight) + junctionType = 1; + else if (hasDown) + junctionType = 2; + else + junctionType = 3; + m_junctionType.append(junctionType); + } + bool isOriginDownward = (m_bd.get_board_type() == BoardType::trigon_3); + m_center = findCenter(bd, points, isOriginDownward); + m_labelPos = QPointF(info.get_label_pos().x, info.get_label_pos().y); +} + +QPointF PieceModel::center() const +{ + return m_center; +} + +int PieceModel::color() +{ + return m_color.to_int(); +} + +QVariantList PieceModel::elements() +{ + return m_elements; +} + +void PieceModel::flipAcrossX() +{ + setTransform(m_bd.get_transforms().get_mirrored_vertically(getTransform())); +} + +void PieceModel::flipAcrossY() +{ + setTransform(m_bd.get_transforms().get_mirrored_horizontally(getTransform())); +} + +QPointF PieceModel::gameCoord() const +{ + return m_gameCoord; +} + +const Transform* PieceModel::getTransform(const QString& state) const +{ + auto variant = m_bd.get_variant(); + bool isTrigon = (variant == Variant::trigon || variant == Variant::trigon_2 + || variant == Variant::trigon_3); + auto& transforms = m_bd.get_transforms(); + // See comment in getTransform() about the mapping between states and + // transform classes. + if (state.isEmpty()) + return isTrigon ? transforms.find() + : transforms.find(); + if (state == QLatin1String("rot60")) + return transforms.find(); + if (state == QLatin1String("rot90")) + return transforms.find(); + if (state == QLatin1String("rot120")) + return transforms.find(); + if (state == QLatin1String("rot180")) + return isTrigon ? transforms.find() + : transforms.find(); + if (state == QLatin1String("rot240")) + return transforms.find(); + if (state == QLatin1String("rot270")) + return transforms.find(); + if (state == QLatin1String("rot300")) + return transforms.find(); + if (state == QLatin1String("flip")) + return isTrigon ? transforms.find() + : transforms.find(); + if (state == QLatin1String("rot60Flip")) + return transforms.find(); + if (state == QLatin1String("rot90Flip")) + return transforms.find(); + if (state == QLatin1String("rot120Flip")) + return transforms.find(); + if (state == QLatin1String("rot180Flip")) + return isTrigon ? transforms.find() + : transforms.find(); + if (state == QLatin1String("rot240Flip")) + return transforms.find(); + if (state == QLatin1String("rot270Flip")) + return transforms.find(); + if (state == QLatin1String("rot300Flip")) + return transforms.find(); + qWarning() << "PieceModel: unknown state " << m_state; + return transforms.find(); +} + +QPointF PieceModel::findCenter(const Board& bd, const PiecePoints& points, + bool isOriginDownward) +{ + auto pieceSet = bd.get_piece_set(); + bool isTrigon = (pieceSet == PieceSet::trigon); + bool isNexos = (pieceSet == PieceSet::nexos); + auto& geo = bd.get_geometry(); + qreal sumX = 0; + qreal sumY = 0; + qreal n = 0; + for (auto& p : points) + { + if (isNexos && geo.get_point_type(p) == 0) + continue; + ++n; + qreal centerX = p.x + 0.5; + qreal centerY; + if (isTrigon) + { + bool isDownward = + (geo.get_point_type(p) == (isOriginDownward ? 0 : 1)); + if (isDownward) + centerY = static_cast(p.y) + 1.f / 3; + else + centerY = static_cast(p.y) + 2.f / 3; + } + else + centerY = p.y + 0.5; + sumX += centerX; + sumY += centerY; + } + return QPointF(sumX / n, sumY / n); +} + +bool PieceModel::isLastMove() const +{ + return m_isLastMove; +} + +bool PieceModel::isPlayed() const +{ + return m_isPlayed; +} + +QVariantList PieceModel::junctions() +{ + return m_junctions; +} + +QVariantList PieceModel::junctionType() +{ + return m_junctionType; +} + +QPointF PieceModel::labelPos() const +{ + return m_labelPos; +} + +void PieceModel::rotateLeft() +{ + setTransform(m_bd.get_transforms().get_rotated_anticlockwise(getTransform())); +} + +void PieceModel::rotateRight() +{ + setTransform(m_bd.get_transforms().get_rotated_clockwise(getTransform())); +} + +void PieceModel::setGameCoord(QPointF gameCoord) +{ + if (m_gameCoord == gameCoord) + return; + m_gameCoord = gameCoord; + emit gameCoordChanged(gameCoord); +} + +void PieceModel::setIsLastMove(bool isLastMove) +{ + if (m_isLastMove == isLastMove) + return; + m_isLastMove = isLastMove; + emit isLastMoveChanged(isLastMove); +} + +void PieceModel::setIsPlayed(bool isPlayed) +{ + if (m_isPlayed == isPlayed) + return; + m_isPlayed = isPlayed; + emit isPlayedChanged(isPlayed); +} + +void PieceModel::setDefaultState() +{ + if (m_state.isEmpty()) + return; + m_state.clear(); + emit stateChanged(m_state); +} + +void PieceModel::setTransform(const Transform* transform) +{ + QString state; + // libboardgame_base uses a different convention for the order of flipping + // and rotation, so the names of the states and transform classes differ + // for flipped states. + if (dynamic_cast(transform) + || dynamic_cast(transform)) + ; + else if (dynamic_cast(transform)) + state = QLatin1String("rot60"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot90"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot120"); + else if (dynamic_cast(transform) + || dynamic_cast(transform)) + state = QLatin1String("rot180"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot240"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot270"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot300"); + else if (dynamic_cast(transform) + || dynamic_cast(transform)) + state = QLatin1String("flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot60Flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot90Flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot120Flip"); + else if (dynamic_cast(transform) + || dynamic_cast(transform)) + state = QLatin1String("rot180Flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot240Flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot270Flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot300Flip"); + else + { + qWarning() << "Invalid Transform " << typeid(*transform).name(); + return; + } + if (m_state == state) + return; + m_state = state; + emit stateChanged(m_state); +} + +QString PieceModel::state() const +{ + return m_state; +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_qml/PieceModel.h b/src/pentobi_qml/PieceModel.h new file mode 100644 index 0000000..dc344fd --- /dev/null +++ b/src/pentobi_qml/PieceModel.h @@ -0,0 +1,133 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/PieceModel.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_QML_PIECE_MODEL_H +#define PENTOBI_QML_PIECE_MODEL_H + +#include +#include +#include +#include "libpentobi_base/Board.h" + +using libboardgame_base::Transform; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::Piece; +using libpentobi_base::PiecePoints; + +//----------------------------------------------------------------------------- + +class PieceModel + : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int color READ color CONSTANT) + Q_PROPERTY(QVariantList elements READ elements CONSTANT) + Q_PROPERTY(QVariantList junctions READ junctions CONSTANT) + Q_PROPERTY(QVariantList junctionType READ junctionType CONSTANT) + Q_PROPERTY(QPointF center READ center CONSTANT) + Q_PROPERTY(QPointF labelPos READ labelPos CONSTANT) + Q_PROPERTY(QString state READ state NOTIFY stateChanged) + Q_PROPERTY(bool isPlayed READ isPlayed NOTIFY isPlayedChanged) + Q_PROPERTY(bool isLastMove READ isLastMove NOTIFY isLastMoveChanged) + Q_PROPERTY(QPointF gameCoord READ gameCoord NOTIFY gameCoordChanged) + +public: + static QPointF findCenter(const Board& bd, const PiecePoints& points, + bool isOriginDownward); + + PieceModel(QObject* parent, const Board& bd, Piece piece, Color c); + + int color(); + + /** List of QPointF instances with coordinates of piece elements. */ + QVariantList elements(); + + /** List of QPointF instances with coordinates of piece junctions. + Only used in Nexos. */ + QVariantList junctions(); + + /** List of integers determining the type of junctions. + In Nexos, this is the type of junction in junction(). In Callisto, it + is the information if the squares in elements() have a right and/or + down neighbor. See implementation for the meaning of the numbers. */ + QVariantList junctionType(); + + QPointF center() const; + + QPointF labelPos() const; + + QString state() const; + + bool isPlayed() const; + + bool isLastMove() const; + + QPointF gameCoord() const; + + Piece getPiece() const { return m_piece; } + + const Transform* getTransform(const QString& state) const; + + const Transform* getTransform() const { return getTransform(m_state); } + + void setDefaultState(); + + void setTransform(const Transform* transform); + + void setIsPlayed(bool isPlayed); + + void setIsLastMove(bool isLastMove); + + void setGameCoord(QPointF gameCoord); + + Q_INVOKABLE void rotateLeft(); + + Q_INVOKABLE void rotateRight(); + + Q_INVOKABLE void flipAcrossX(); + + Q_INVOKABLE void flipAcrossY(); + +signals: + void stateChanged(QString); + + void isPlayedChanged(bool); + + void isLastMoveChanged(bool); + + void gameCoordChanged(QPointF); + +private: + const Board& m_bd; + + Color m_color; + + Piece m_piece; + + bool m_isPlayed = false; + + bool m_isLastMove = false; + + QPointF m_gameCoord; + + QPointF m_center; + + QPointF m_labelPos; + + QVariantList m_elements; + + QVariantList m_junctions; + + QVariantList m_junctionType; + + QString m_state; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_QML_PIECE_MODEL_H diff --git a/src/pentobi_qml/PlayerModel.cpp b/src/pentobi_qml/PlayerModel.cpp new file mode 100644 index 0000000..a21cb98 --- /dev/null +++ b/src/pentobi_qml/PlayerModel.cpp @@ -0,0 +1,228 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/PlayerModel.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "PlayerModel.h" + +#include +#include +#include +#include + +using namespace std; +using libboardgame_util::clear_abort; +using libboardgame_util::set_abort; + +//----------------------------------------------------------------------------- + +namespace { + +unsigned maxLevel = 7; + +void getLevel(QSettings& settings, const char* key, unsigned& level) +{ + level = settings.value(key, 1).toUInt(); + if (level < 1) + { + qDebug() << "PlayerModel: invalid level in settings:" << level; + level = 1; + } + else if (level > maxLevel) + { + qDebug() << "PlayerModel: level in settings too high, using level" + << maxLevel; + level = maxLevel; + } +} + +} // namespace + +//----------------------------------------------------------------------------- + +bool PlayerModel::noBook = false; + +bool PlayerModel::noDelay = false; + +unsigned PlayerModel::nuThreads = 0; + +PlayerModel::PlayerModel(QObject* parent) + : QObject(parent), + m_player(GameModel::getInitialGameVariant(), maxLevel, "", nuThreads) +{ + if (noBook) + m_player.set_use_book(false); + QSettings settings; + getLevel(settings, "level_classic", m_levelClassic); + getLevel(settings, "level_classic_2", m_levelClassic2); + getLevel(settings, "level_classic_3", m_levelClassic3); + getLevel(settings, "level_duo", m_levelDuo); + getLevel(settings, "level_trigon", m_levelTrigon); + getLevel(settings, "level_trigon_2", m_levelTrigon2); + getLevel(settings, "level_trigon_3", m_levelTrigon3); + getLevel(settings, "level_junior", m_levelJunior); + getLevel(settings, "level_nexos", m_levelNexos); + getLevel(settings, "level_nexos_2", m_levelNexos2); + getLevel(settings, "level_callisto", m_levelCallisto); + getLevel(settings, "level_callisto_2", m_levelCallisto2); + getLevel(settings, "level_callisto_3", m_levelCallisto3); + connect(&m_genMoveWatcher, SIGNAL(finished()), SLOT(genMoveFinished())); +} + +PlayerModel::~PlayerModel() +{ + cancelGenMove(); + QSettings settings; + settings.setValue("level_classic", m_levelClassic); + settings.setValue("level_classic_2", m_levelClassic2); + settings.setValue("level_classic_3", m_levelClassic3); + settings.setValue("level_duo", m_levelDuo); + settings.setValue("level_trigon", m_levelTrigon); + settings.setValue("level_trigon_2", m_levelTrigon2); + settings.setValue("level_trigon_3", m_levelTrigon3); + settings.setValue("level_junior", m_levelJunior); + settings.setValue("level_nexos", m_levelNexos); + settings.setValue("level_nexos_2", m_levelNexos2); + settings.setValue("level_callisto", m_levelCallisto); + settings.setValue("level_callisto_2", m_levelCallisto2); + settings.setValue("level_callisto_3", m_levelCallisto3); +} + +PlayerModel::GenMoveResult PlayerModel::asyncGenMove(GameModel* gm, + unsigned genMoveId) +{ + QElapsedTimer timer; + timer.start(); + auto& bd = gm->getBoard(); + GenMoveResult result; + result.genMoveId = genMoveId; + result.gameModel = gm; + result.move = m_player.genmove(bd, bd.get_effective_to_play()); + auto elapsed = timer.elapsed(); + // Enforce minimum thinking time of 1 sec + if (elapsed < 1000 && ! noDelay) + QThread::msleep(1000 - elapsed); + return result; +} + +void PlayerModel::cancelGenMove() +{ + if (! m_isGenMoveRunning) + return; + // After waitForFinished() returns, we can be sure that the move generation + // is no longer running, but we will still receive the finished event. + // Increasing m_genMoveId will make genMoveFinished() ignore the event. + ++m_genMoveId; + set_abort(); + m_genMoveWatcher.waitForFinished(); + setIsGenMoveRunning(false); +} + +void PlayerModel::genMoveFinished() +{ + auto result = m_genMoveWatcher.future().result(); + if (result.genMoveId != m_genMoveId) + // Callback from a canceled move generation + return; + setIsGenMoveRunning(false); + auto& bd = result.gameModel->getBoard(); + auto mv = result.move; + if (mv.is_null()) + { + qWarning("PlayerModel: failed to generate move"); + return; + } + Color c = bd.get_effective_to_play(); + if (! bd.is_legal(c, mv)) + { + qWarning("PlayerModel: player generated illegal move"); + return; + } + emit moveGenerated(mv.to_int()); +} + +void PlayerModel::loadBook(Variant variant) +{ + QFile file(QString(":/pentobi_books/book_%1.blksgf") + .arg(to_string_id(variant))); + if (! file.open(QIODevice::ReadOnly)) + { + qWarning() << "PlayerModel: could not open " << file.fileName(); + return; + } + QTextStream stream(&file); + QString text = stream.readAll(); + istringstream in(text.toLocal8Bit().constData()); + m_player.load_book(in); +} + +void PlayerModel::setIsGenMoveRunning(bool isGenMoveRunning) +{ + if (m_isGenMoveRunning == isGenMoveRunning) + return; + m_isGenMoveRunning = isGenMoveRunning; + emit isGenMoveRunningChanged(isGenMoveRunning); +} + +void PlayerModel::startGenMove(GameModel* gm) +{ + unsigned level; + switch (gm->getBoard().get_variant()) + { + case Variant::classic_2: + level = m_levelClassic2; + break; + case Variant::classic_3: + level = m_levelClassic3; + break; + case Variant::duo: + level = m_levelDuo; + break; + case Variant::trigon: + level = m_levelTrigon; + break; + case Variant::trigon_2: + level = m_levelTrigon2; + break; + case Variant::trigon_3: + level = m_levelTrigon3; + break; + case Variant::nexos: + level = m_levelNexos; + break; + case Variant::nexos_2: + level = m_levelNexos2; + break; + case Variant::callisto: + level = m_levelCallisto; + break; + case Variant::callisto_2: + level = m_levelCallisto2; + break; + case Variant::callisto_3: + level = m_levelCallisto3; + break; + default: + level = m_levelClassic; + } + startGenMoveAtLevel(gm, level); +} + +void PlayerModel::startGenMoveAtLevel(GameModel* gm, unsigned level) +{ + cancelGenMove(); + m_player.set_level(level); + auto variant = gm->getBoard().get_variant(); + if (! m_player.is_book_loaded(variant)) + loadBook(variant); + clear_abort(); + ++m_genMoveId; + QFuture future = + QtConcurrent::run(this, &PlayerModel::asyncGenMove, gm, + m_genMoveId); + m_genMoveWatcher.setFuture(future); + setIsGenMoveRunning(true); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_qml/PlayerModel.h b/src/pentobi_qml/PlayerModel.h new file mode 100644 index 0000000..b08cebe --- /dev/null +++ b/src/pentobi_qml/PlayerModel.h @@ -0,0 +1,173 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/PlayerModel.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_QML_PLAYER_MODEL_H +#define PENTOBI_QML_PLAYER_MODEL_H + +#include +#include "GameModel.h" +#include "libpentobi_mcts/Player.h" + +using namespace std; +using libpentobi_mcts::Player; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class PlayerModel + : public QObject +{ + Q_OBJECT + Q_PROPERTY(unsigned levelClassic MEMBER m_levelClassic + NOTIFY levelClassicChanged) + Q_PROPERTY(unsigned levelClassic2 MEMBER m_levelClassic2 + NOTIFY levelClassic2Changed) + Q_PROPERTY(unsigned levelClassic3 MEMBER m_levelClassic3 + NOTIFY levelClassic3Changed) + Q_PROPERTY(unsigned levelDuo MEMBER m_levelDuo NOTIFY levelDuoChanged) + Q_PROPERTY(unsigned levelTrigon MEMBER m_levelTrigon + NOTIFY levelTrigonChanged) + Q_PROPERTY(unsigned levelTrigon2 MEMBER m_levelTrigon2 + NOTIFY levelTrigon2Changed) + Q_PROPERTY(unsigned levelTrigon3 MEMBER m_levelTrigon3 + NOTIFY levelTrigon3Changed) + Q_PROPERTY(unsigned levelJunior MEMBER m_levelJunior + NOTIFY levelJuniorChanged) + Q_PROPERTY(unsigned levelNexos MEMBER m_levelNexos NOTIFY + levelNexosChanged) + Q_PROPERTY(unsigned levelNexos2 MEMBER m_levelNexos2 NOTIFY + levelNexos2Changed) + Q_PROPERTY(unsigned levelCallisto MEMBER m_levelCallisto + NOTIFY levelCallistoChanged) + Q_PROPERTY(unsigned levelCallisto2 MEMBER m_levelCallisto2 + NOTIFY levelCallisto2Changed) + Q_PROPERTY(unsigned levelCallisto3 MEMBER m_levelCallisto3 + NOTIFY levelCallisto3Changed) + Q_PROPERTY(bool isGenMoveRunning MEMBER m_isGenMoveRunning + NOTIFY isGenMoveRunningChanged) + +public: + /** Global variable to disable opening books. */ + static bool noBook; + + /** Global variable to disable the minimum thinking time. */ + static bool noDelay; + + /** Global variable to set the number of threads the player is constructed + with. + The default value 0 means that the number of threads depends on the + hardware. */ + static unsigned nuThreads; + + + explicit PlayerModel(QObject* parent = nullptr); + + ~PlayerModel(); + + + /** Start a move generation in a background thread. + The state of the board model may not be changed until the move + generation was finished (computerPlayed signal) or aborted + with cancelGenMove() */ + Q_INVOKABLE void startGenMove(GameModel* gameModel); + + Q_INVOKABLE void startGenMoveAtLevel(GameModel* gameModel, unsigned level); + + /** Cancel the move generation in the background thread if one is + running. */ + Q_INVOKABLE void cancelGenMove(); + +signals: + void levelCallistoChanged(unsigned); + + void levelCallisto2Changed(unsigned); + + void levelCallisto3Changed(unsigned); + + void levelClassicChanged(unsigned); + + void levelClassic2Changed(unsigned); + + void levelClassic3Changed(unsigned); + + void levelDuoChanged(unsigned); + + void levelTrigonChanged(unsigned); + + void levelTrigon2Changed(unsigned); + + void levelTrigon3Changed(unsigned); + + void levelJuniorChanged(unsigned); + + void levelNexosChanged(unsigned); + + void levelNexos2Changed(unsigned); + + void isGenMoveRunningChanged(bool); + + void moveGenerated(int move); + +private: + struct GenMoveResult + { + Color color; + + Move move; + + unsigned genMoveId; + + GameModel* gameModel; + }; + + bool m_isGenMoveRunning = false; + + unsigned m_levelCallisto; + + unsigned m_levelCallisto2; + + unsigned m_levelCallisto3; + + unsigned m_levelClassic; + + unsigned m_levelClassic2; + + unsigned m_levelClassic3; + + unsigned m_levelDuo; + + unsigned m_levelTrigon; + + unsigned m_levelTrigon2; + + unsigned m_levelTrigon3; + + unsigned m_levelJunior; + + unsigned m_levelNexos; + + unsigned m_levelNexos2; + + unsigned m_genMoveId = 0; + + Player m_player; + + QFutureWatcher m_genMoveWatcher; + + + GenMoveResult asyncGenMove(GameModel* gm, unsigned genMoveId); + + void loadBook(Variant variant); + + void setIsGenMoveRunning(bool isGenMoveRunning); + +private slots: + void genMoveFinished(); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_QML_PLAYER_MODEL_H diff --git a/src/pentobi_qml/android/AndroidManifest.xml b/src/pentobi_qml/android/AndroidManifest.xml new file mode 100644 index 0000000..60bb1c7 --- /dev/null +++ b/src/pentobi_qml/android/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/android/res/drawable-hdpi/icon.png b/src/pentobi_qml/android/res/drawable-hdpi/icon.png new file mode 100644 index 0000000..4a5bd8e Binary files /dev/null and b/src/pentobi_qml/android/res/drawable-hdpi/icon.png differ diff --git a/src/pentobi_qml/android/res/drawable-mdpi/icon.png b/src/pentobi_qml/android/res/drawable-mdpi/icon.png new file mode 100644 index 0000000..4fb4397 Binary files /dev/null and b/src/pentobi_qml/android/res/drawable-mdpi/icon.png differ diff --git a/src/pentobi_qml/android/res/drawable/splash.xml b/src/pentobi_qml/android/res/drawable/splash.xml new file mode 100644 index 0000000..32f67f9 --- /dev/null +++ b/src/pentobi_qml/android/res/drawable/splash.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/pentobi_qml/android/res/values/theme.xml b/src/pentobi_qml/android/res/values/theme.xml new file mode 100644 index 0000000..adec232 --- /dev/null +++ b/src/pentobi_qml/android/res/values/theme.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/pentobi_qml/android_icons_svg/icon48.svg b/src/pentobi_qml/android_icons_svg/icon48.svg new file mode 100644 index 0000000..655a58e --- /dev/null +++ b/src/pentobi_qml/android_icons_svg/icon48.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/android_icons_svg/icon72.svg b/src/pentobi_qml/android_icons_svg/icon72.svg new file mode 100644 index 0000000..3575ed4 --- /dev/null +++ b/src/pentobi_qml/android_icons_svg/icon72.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/deployment.pri b/src/pentobi_qml/deployment.pri new file mode 100644 index 0000000..5441b63 --- /dev/null +++ b/src/pentobi_qml/deployment.pri @@ -0,0 +1,27 @@ +android-no-sdk { + target.path = /data/user/qt + export(target.path) + INSTALLS += target +} else:android { + x86 { + target.path = /libs/x86 + } else: armeabi-v7a { + target.path = /libs/armeabi-v7a + } else { + target.path = /libs/armeabi + } + export(target.path) + INSTALLS += target +} else:unix { + isEmpty(target.path) { + qnx { + target.path = /tmp/$${TARGET}/bin + } else { + target.path = /opt/$${TARGET}/bin + } + export(target.path) + } + INSTALLS += target +} + +export(INSTALLS) diff --git a/src/pentobi_qml/icons_android.qrc b/src/pentobi_qml/icons_android.qrc new file mode 100644 index 0000000..c3a99c7 --- /dev/null +++ b/src/pentobi_qml/icons_android.qrc @@ -0,0 +1,15 @@ + + + qml/icons/menu.svg + qml/icons/pentobi-backward.svg + qml/icons/pentobi-beginning.svg + qml/icons/pentobi-computer-colors.svg + qml/icons/pentobi-end.svg + qml/icons/pentobi-forward.svg + qml/icons/pentobi-newgame.svg + qml/icons/pentobi-next-variation.svg + qml/icons/pentobi-play.svg + qml/icons/pentobi-previous-variation.svg + qml/icons/pentobi-undo.svg + + diff --git a/src/pentobi_qml/qml/.gitignore b/src/pentobi_qml/qml/.gitignore new file mode 100644 index 0000000..8df47d5 --- /dev/null +++ b/src/pentobi_qml/qml/.gitignore @@ -0,0 +1 @@ +*.qm diff --git a/src/pentobi_qml/qml/AndroidToolBar.qml b/src/pentobi_qml/qml/AndroidToolBar.qml new file mode 100644 index 0000000..f0bf836 --- /dev/null +++ b/src/pentobi_qml/qml/AndroidToolBar.qml @@ -0,0 +1,46 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.0 +import "Main.js" as Logic + +RowLayout { + function popupMenu() { menu.popup() } + + spacing: 0 + + Item { Layout.fillWidth: true } + AndroidToolButton { + imageSource: "icons/pentobi-newgame.svg" + visible: ! gameModel.isGameEmpty + onClicked: Logic.newGame() + } + AndroidToolButton { + visible: gameModel.canUndo + imageSource: "icons/pentobi-undo.svg" + onClicked: Logic.undo() + } + AndroidToolButton { + imageSource: "icons/pentobi-computer-colors.svg" + onClicked: Logic.showComputerColorDialog() + } + AndroidToolButton { + visible: ! gameModel.isGameOver + imageSource: "icons/pentobi-play.svg" + onClicked: Logic.computerPlay() + } + AndroidToolButton { + imageSource: "icons/menu.svg" + menu: menu + } + Menu { + id: menu + + MenuGame { } + MenuGo { } + MenuEdit { } + MenuComputer { } + MenuView { } + MenuHelp { } + } +} diff --git a/src/pentobi_qml/qml/AndroidToolButton.qml b/src/pentobi_qml/qml/AndroidToolButton.qml new file mode 100644 index 0000000..44f21b3 --- /dev/null +++ b/src/pentobi_qml/qml/AndroidToolButton.qml @@ -0,0 +1,18 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import QtQuick.Window 2.0 + +ToolButton { + property string imageSource + + Image { + // We currently use 22x22 SVG files, try to use 22x22 or 44x44, unless + // very high DPI + width: Screen.pixelDensity < 5 ? 22 : Screen.pixelDensity < 10 ? 44 : 5 * Screen.pixelDensity + height: width + sourceSize { width: width; height: height } + anchors.centerIn: parent + source: imageSource + cache: false + } +} diff --git a/src/pentobi_qml/qml/Board.qml b/src/pentobi_qml/qml/Board.qml new file mode 100644 index 0000000..3e818d9 --- /dev/null +++ b/src/pentobi_qml/qml/Board.qml @@ -0,0 +1,208 @@ +import QtQuick 2.0 + +Item { + id: root + + property string gameVariant + property bool isTrigon: gameVariant.indexOf("trigon") === 0 + property bool isNexos: gameVariant.indexOf("nexos") === 0 + property bool isCallisto: gameVariant.indexOf("callisto") === 0 + property int columns: { + switch (gameVariant) { + case "duo": + case "junior": + return 14 + case "callisto_2": + return 16 + case "trigon": + case "trigon_2": + return 35 + case "trigon_3": + return 31 + case "nexos": + case "nexos_2": + return 25 + default: + return 20 + } + } + property int rows: { + switch (gameVariant) { + case "duo": + case "junior": + return 14 + case "callisto_2": + return 16 + case "trigon": + case "trigon_2": + return 18 + case "trigon_3": + return 16 + case "nexos": + case "nexos_2": + return 25 + default: + return 20 + } + } + // Avoid fractional piece element sizes if the piece elements are squares + property real gridWidth: { + var sideLength + if (isTrigon) sideLength = Math.min(width, Math.sqrt(3) * height) + else sideLength = Math.min(width, height) + if (isTrigon) return sideLength / (columns + 1) + else if (isNexos) Math.floor(sideLength / (columns - 0.5)) + else return Math.floor(sideLength / columns) + } + property real gridHeight: { + if (isTrigon) return Math.sqrt(3) * gridWidth + else return gridWidth + } + property real startingPointSize: { + if (isTrigon) return 0.27 * gridHeight + if (isNexos) return 0.3 * gridHeight + return 0.35 * gridHeight + } + + function mapFromGameX(x) { + if (isTrigon) return image.x + (x + 0.5) * gridWidth + else if (isNexos) return image.x + (x - 0.25) * gridWidth + else return image.x + x * gridWidth + } + function mapFromGameY(y) { + if (isNexos) return image.y + (y - 0.25) * gridHeight + else return image.y + y * gridHeight + } + function mapToGame(pos) { + if (isTrigon) + return Qt.point((pos.x - image.x - 0.5 * gridWidth) / gridWidth, + (pos.y - image.y) / gridHeight) + else if (isNexos) + return Qt.point((pos.x - image.x + 0.25 * gridWidth) / gridWidth, + (pos.y - image.y + 0.25 * gridHeight) / gridHeight) + else + return Qt.point((pos.x - image.x) / gridWidth, + (pos.y - image.y) / gridHeight) + } + function getCenterYTrigon(pos) { + + var isDownward = ((pos.x % 2 == 0) != (pos.y % 2 == 0)) + if (gameVariant === "trigon_3") + isDownward = ! isDownward + return (isDownward ? 1 : 2) / 3 * gridHeight + } + + Image { + id: image + + width: { + if (isTrigon) return gridWidth * (columns + 1) + else if (isNexos) return gridWidth * (columns - 0.5) + else return gridWidth * columns + } + height: { + if (isNexos) return gridHeight * (rows - 0.5) + else return gridHeight * rows + } + anchors.centerIn: root + source: { + switch (gameVariant) { + case "trigon": + case "trigon_2": + return theme.getImage("board-trigon") + case "trigon_3": + return theme.getImage("board-trigon-3") + case "nexos": + case "nexos_2": + return theme.getImage("board-tile-nexos") + case "callisto": + return theme.getImage("board-callisto") + case "callisto_2": + return theme.getImage("board-callisto-2") + case "callisto_3": + return theme.getImage("board-callisto-3") + default: + return theme.getImage("board-tile-classic") + } + } + sourceSize { + width: { + if (isTrigon || isCallisto) return width + if (isNexos) return 2 * gridWidth + return gridWidth + } + height: { + if (isTrigon || isCallisto) return height + if (isNexos) return 2 * gridHeight + return gridHeight + } + } + // It should work to use Image.Tile for all game variants, but the + // Trigon board is not painted with Image.width/height even if + // sourceSize is bound to it (the Trigon SVG files have a different + // aspect ratio but that shouldn't matter). Bug in Qt 5.6? + fillMode: isTrigon? Image.Stretch : Image.Tile + horizontalAlignment: Image.AlignLeft + verticalAlignment: Image.AlignTop + cache: false + } + Repeater { + model: gameModel.startingPoints0 + + Rectangle { + color: theme.colorBlue + width: startingPointSize; height: width + radius: width / 2 + x: mapFromGameX(modelData.x) + (gridWidth - width) / 2 + y: mapFromGameY(modelData.y) + (gridHeight - height) / 2 + } + } + Repeater { + model: gameModel.startingPoints1 + + Rectangle { + color: gameModel.gameVariant == "duo" + || gameModel.gameVariant == "junior" + || gameModel.gameVariant == "callisto_2" ? + theme.colorGreen : theme.colorYellow + width: startingPointSize; height: width + radius: width / 2 + x: mapFromGameX(modelData.x) + (gridWidth - width) / 2 + y: mapFromGameY(modelData.y) + (gridHeight - height) / 2 + } + } + Repeater { + model: gameModel.startingPoints2 + + Rectangle { + color: theme.colorRed + width: startingPointSize; height: width + radius: width / 2 + x: mapFromGameX(modelData.x) + (gridWidth - width) / 2 + y: mapFromGameY(modelData.y) + (gridHeight - height) / 2 + } + } + Repeater { + model: gameModel.startingPoints3 + + Rectangle { + color: theme.colorGreen + width: startingPointSize; height: width + radius: width / 2 + x: mapFromGameX(modelData.x) + (gridWidth - width) / 2 + y: mapFromGameY(modelData.y) + (gridHeight - height) / 2 + } + } + Repeater { + model: gameModel.startingPointsAll + + Rectangle { + color: theme.colorStartingPoint + width: startingPointSize; height: width + radius: width / 2 + x: mapFromGameX(modelData.x) + (gridWidth - width) / 2 + y: mapFromGameY(modelData.y) + getCenterYTrigon(modelData) + - height / 2 + } + } +} diff --git a/src/pentobi_qml/qml/Button.qml b/src/pentobi_qml/qml/Button.qml new file mode 100644 index 0000000..172c5b0 --- /dev/null +++ b/src/pentobi_qml/qml/Button.qml @@ -0,0 +1,29 @@ +import QtQuick 2.0 +import QtQuick.Window 2.0 +import Qt.labs.controls 1.0 as Controls2 + +/** Button that supports an automatically scaled image. + The image source should be a SVG file with size 22x22. */ +Controls2.Button { + id: root + + property string imageSource + + label: Image { + sourceSize { + // We currently use 22x22 SVG files, try to use 22x22 or 44x44, unless + // very high DPI + width: Screen.pixelDensity < 5 ? 22 : Screen.pixelDensity < 10 ? 44 : 5 * Screen.pixelDensity + height: Screen.pixelDensity < 5 ? 22 : Screen.pixelDensity < 10 ? 44 : 5 * Screen.pixelDensity + } + fillMode: Image.PreserveAspectFit + source: imageSource + opacity: root.enabled ? 1 : 0.4 + cache: false + } + background: Rectangle { + anchors.fill: root + visible: pressed + color: theme.backgroundButtonPressed + } +} diff --git a/src/pentobi_qml/qml/ComputerColorDialog.qml b/src/pentobi_qml/qml/ComputerColorDialog.qml new file mode 100644 index 0000000..db18fcd --- /dev/null +++ b/src/pentobi_qml/qml/ComputerColorDialog.qml @@ -0,0 +1,82 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import QtQuick.Dialogs 1.2 + +Dialog { + property string gameVariant + property alias computerPlays0: checkBox0.checked + property alias computerPlays1: checkBox1.checked + property alias computerPlays2: checkBox2.checked + property alias computerPlays3: checkBox3.checked + + title: qsTr("Computer Colors") + standardButtons: StandardButton.Ok | StandardButton.Cancel + + GroupBox { + title: qsTr("Computer plays:") + flat: true + + Column { + CheckBox { + id: checkBox0 + + text: { + switch (gameVariant) { + case "classic_2": + case "trigon_2": + case "nexos_2": + return qsTr("Blue/Red") + default: + qsTr("Blue") + } + } + onClicked: { + if (gameVariant == "classic_2" || gameVariant == "trigon_2" + || gameVariant == "nexos_2") + computerPlays2 = checked + } + } + CheckBox { + id: checkBox1 + + text: { + switch (gameVariant) { + case "classic_2": + case "trigon_2": + case "nexos_2": + return qsTr("Yellow/Green") + case "duo": + case "junior": + case "callisto_2": + return qsTr("Green") + default: + qsTr("Yellow") + } + } + onClicked: { + if (gameVariant == "classic_2" || gameVariant == "trigon_2" + || gameVariant == "nexos_2") + computerPlays3 = checked + } + } + CheckBox { + id: checkBox2 + + text: qsTr("Red") + visible: gameVariant == "classic" || gameVariant == "trigon" + || gameVariant == "trigon_3" + || gameVariant == "classic_3" + || gameVariant == "nexos" + || gameVariant == "callisto_3" + || gameVariant == "callisto" + } + CheckBox { + id: checkBox3 + + text: qsTr("Green") + visible: gameVariant == "classic" || gameVariant == "trigon" + || gameVariant == "nexos" || gameVariant == "callisto" + } + } + } +} diff --git a/src/pentobi_qml/qml/GameDisplay.js b/src/pentobi_qml/qml/GameDisplay.js new file mode 100644 index 0000000..58e3d0f --- /dev/null +++ b/src/pentobi_qml/qml/GameDisplay.js @@ -0,0 +1,108 @@ +function createColorPieces(component, pieceModels) { + if (pieceModels.length === 0) + return [] + var colorName + switch (pieceModels[0].color) { + case 0: colorName = "blue"; break + case 1: + colorName = gameModel.gameVariant == "duo" + || gameModel.gameVariant == "junior" + || gameModel.gameVariant == "callisto_2" ? + "green" : "yellow"; break + case 2: colorName = "red"; break + case 3: colorName = "green"; break + } + var properties = { + "colorName": colorName, + "isPicked": Qt.binding(function() { return this === pickedPiece }), + "isMarked": Qt.binding(function() { + return markLastMove && this.pieceModel.isLastMove }) + } + var pieces = [] + for (var i = 0; i < pieceModels.length; ++i) { + properties["pieceModel"] = pieceModels[i] + pieces.push(component.createObject(gameDisplay, properties)) + } + return pieces +} + +function createPieces() { + var file + if (gameModel.gameVariant.indexOf("trigon") === 0) + file = "PieceTrigon.qml" + else if (gameModel.gameVariant.indexOf("nexos") === 0) + file = "PieceNexos.qml" + else if (gameModel.gameVariant.indexOf("callisto") === 0) + file = "PieceCallisto.qml" + else + file = "PieceClassic.qml" + var component = Qt.createComponent(file) + pieces0 = createColorPieces(component, gameModel.pieceModels0) + pieces1 = createColorPieces(component, gameModel.pieceModels1) + pieces2 = createColorPieces(component, gameModel.pieceModels2) + pieces3 = createColorPieces(component, gameModel.pieceModels3) + pieceSelector.transitionsEnabled = + Qt.binding(function() { return enableAnimations }) +} + +function destroyColorPieces(pieces) { + if (pieces === undefined) + return + for (var i = 0; i < pieces.length; ++i) { + pieces[i].visible = false + pieces[i].destroy(1000) + } +} + +function destroyPieces() { + pieceSelector.transitionsEnabled = false + pickedPiece = null + destroyColorPieces(pieces0); pieces0 = [] + destroyColorPieces(pieces1); pieces1 = [] + destroyColorPieces(pieces2); pieces2 = [] + destroyColorPieces(pieces3); pieces3 = [] +} + +function findPiece(pieceModel, color) { + var pieces + switch (color) { + case 0: pieces = pieces0; break + case 1: pieces = pieces1; break + case 2: pieces = pieces2; break + case 3: pieces = pieces3; break + } + if (pieces === undefined) + return null // Pieces haven't been created yet + for (var i = 0; i < pieces.length; ++i) + if (pieces[i].pieceModel === pieceModel) + return pieces[i] + return null +} + +function pickPiece(piece) { + if (playerModel.isGenMoveRunning || gameModel.isGameOver + || piece.pieceModel.color !== gameModel.toPlay) + return + if (! pieceManipulator.visible) { + // Position pieceManipulator at center of piece if possible, but + // make sure it is completely visible + var newCoord = mapFromItem(piece, 0, 0) + var x = newCoord.x - pieceManipulator.width / 2 + var y = newCoord.y - pieceManipulator.height / 2 + x = Math.max(Math.min(x, width - pieceManipulator.width), 0) + y = Math.max(Math.min(y, height - pieceManipulator.height), 0) + pieceManipulator.x = x + pieceManipulator.y = y + } + pickedPiece = piece +} + +function showMoveHint(move) { + var pieceModel = gameModel.preparePiece(gameModel.toPlay, move) + var pos = board.mapToItem(pieceManipulator.parent, + board.mapFromGameX(pieceModel.gameCoord.x), + board.mapFromGameY(pieceModel.gameCoord.y)) + pieceManipulator.x = pos.x - pieceManipulator.width / 2 + pieceManipulator.y = pos.y - pieceManipulator.height / 2 + pickedPiece = findPiece(pieceModel, gameModel.toPlay) +} diff --git a/src/pentobi_qml/qml/GameDisplay.qml b/src/pentobi_qml/qml/GameDisplay.qml new file mode 100644 index 0000000..412dd68 --- /dev/null +++ b/src/pentobi_qml/qml/GameDisplay.qml @@ -0,0 +1,157 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "GameDisplay.js" as Logic + +Item +{ + id: gameDisplay // Referenced by Piece*.qml + + property var pickedPiece: null + property bool markLastMove: true + property bool enableAnimations: true + property alias busyIndicatorRunning: busyIndicator.running + property size imageSourceSize: { + var width = board.gridWidth, height = board.gridHeight + if (board.isTrigon) + return Qt.size(2 * width, height) + if (board.isNexos) + return Qt.size(1.5 * width, 1.5 * height) + if (board.isCallisto) + return Qt.size(0.9 * width, 0.9 * height) + return Qt.size(width, height) + } + property alias pieces0: pieceSelector.pieces0 + property alias pieces1: pieceSelector.pieces1 + property alias pieces2: pieceSelector.pieces2 + property alias pieces3: pieceSelector.pieces3 + + signal play(var pieceModel, point gameCoord) + + function createPieces() { Logic.createPieces() } + function destroyPieces() { Logic.destroyPieces() } + function showToPlay() { pieceSelector.contentY = 0 } + function showMoveHint(move) { Logic.showMoveHint(move) } + + onWidthChanged: pickedPiece = null + onHeightChanged: pickedPiece = null + + Column { + id: column + + width: gameDisplay.width + anchors.centerIn: gameDisplay + spacing: 0.01 * board.width + + Board { + id: board + + gameVariant: gameModel.gameVariant + width: Math.min( + parent.width, + gameDisplay.height / (1.07 + 2.7 / pieceSelector.columns)) + height: isTrigon ? Math.sqrt(3) / 2 * width : width + anchors.horizontalCenter: parent.horizontalCenter + } + ScoreDisplay { + id: scoreDisplay + + gameVariant: gameModel.gameVariant + points0: gameModel.points0 + points1: gameModel.points1 + points2: gameModel.points2 + points3: gameModel.points3 + bonus0: gameModel.bonus0 + bonus1: gameModel.bonus1 + bonus2: gameModel.bonus2 + bonus3: gameModel.bonus3 + hasMoves0: gameModel.hasMoves0 + hasMoves1: gameModel.hasMoves1 + hasMoves2: gameModel.hasMoves2 + hasMoves3: gameModel.hasMoves3 + toPlay: gameModel.isGameOver ? -1 : gameModel.toPlay + altPlayer: gameModel.altPlayer + height: board.width / 20 + pointSize: 0.6 * height + anchors.horizontalCenter: parent.horizontalCenter + } + Flickable { + id: flickable + + width: 0.9 * board.width + height: width / pieceSelector.columns * pieceSelector.rows + contentWidth: 2 * width + contentHeight: height + anchors.horizontalCenter: board.horizontalCenter + clip: true + onMovementEnded: { + snapAnimation.to = contentX > width / 2 ? width : 0 + snapAnimation.restart() + } + + Row { + id: flickableContent + + PieceSelector { + id: pieceSelector + + columns: gameModel.gameVariant.indexOf("classic") == 0 + || gameModel.gameVariant.indexOf("callisto") == 0 + || gameModel.gameVariant == "duo" ? 7 : 8 + width: flickable.width + height: flickable.height + rows: 3 + gameVariant: gameModel.gameVariant + toPlay: gameModel.toPlay + nuColors: gameModel.nuColors + transitionsEnabled: false + onPiecePicked: Logic.pickPiece(piece) + } + NavigationPanel { + width: flickable.width + height: flickable.height + } + } + SmoothedAnimation { + id: snapAnimation + + target: flickable + property: "contentX" + duration: 200 + } + } + } + BusyIndicator { + id: busyIndicator + + x: (gameDisplay.width - width) / 2 + y: column.y + flickable.y + (flickable.height - height) / 2 + } + PieceManipulator { + id: pieceManipulator + + legal: { + if (pickedPiece === null) + return false + // Don't use mapToItem(board, width / 2, height / 2), we want a + // dependency on x, y. + var pos = parent.mapToItem(board, x + width / 2, y + height / 2) + return gameModel.isLegalPos(pickedPiece.pieceModel, + pickedPiece.pieceModel.state, + board.mapToGame(pos)) + } + width: 0.6 * board.width; height: width + visible: pickedPiece !== null + pieceModel: pickedPiece !== null ? pickedPiece.pieceModel : null + onPiecePlayed: { + var pos = mapToItem(board, width / 2, height / 2) + if (! board.contains(Qt.point(pos.x, pos.y))) + pickedPiece = null + else if (legal) + play(pieceModel, board.mapToGame(pos)) + } + } + Connections { + target: gameModel + onPositionChanged: pickedPiece = null + } +} diff --git a/src/pentobi_qml/qml/LineSegment.qml b/src/pentobi_qml/qml/LineSegment.qml new file mode 100644 index 0000000..b309853 --- /dev/null +++ b/src/pentobi_qml/qml/LineSegment.qml @@ -0,0 +1,105 @@ +import QtQuick 2.3 + +// Piece element for Nexos. See Square.qml for comments. +Item { + id: root + + property bool isHorizontal + + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component0 + } + + anchors.fill: root + opacity: imageOpacity0 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component0 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + mirror: ! isHorizontal + rotation: isHorizontal ? 0 : -90 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component90 + } + + anchors.fill: root + opacity: imageOpacity90 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component90 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + mirror: isHorizontal + rotation: isHorizontal ? -180 : -90 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component180 + } + + anchors.fill: root + opacity: imageOpacity180 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component180 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + mirror: ! isHorizontal + rotation: isHorizontal ? -180 : -270 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component270 + } + + anchors.fill: root + opacity: imageOpacity270 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component270 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + mirror: isHorizontal + rotation: isHorizontal ? 0 : -270 + } + } + } +} diff --git a/src/pentobi_qml/qml/Main.js b/src/pentobi_qml/qml/Main.js new file mode 100644 index 0000000..d6a3963 --- /dev/null +++ b/src/pentobi_qml/qml/Main.js @@ -0,0 +1,274 @@ +function about() { + var url = "http://pentobi.sourceforge.net" + showInfo("

" + qsTr("Pentobi") + "

" + + qsTr("Version %1").arg(Qt.application.version) + "

" + + qsTr("Computer opponent for the board game Blokus.") + "
" + + qsTr("© 2011–%1 Markus Enzenberger").arg(2017) + + "
" + url + "

") +} + +function changeGameVariant(gameVariant) { + if (gameModel.gameVariant === gameVariant) + return + if (! gameModel.isGameEmpty && ! gameModel.isGameOver) { + showQuestion(qsTr("New game?"), + function() { changeGameVariantNoVerify(gameVariant) }) + return + } + changeGameVariantNoVerify(gameVariant) +} + +function changeGameVariantNoVerify(gameVariant) { + cancelGenMove() + lengthyCommand.run(function() { + gameDisplay.destroyPieces() + gameModel.initGameVariant(gameVariant) + gameDisplay.createPieces() + gameDisplay.showToPlay() + initComputerColors() + }) +} + +function checkComputerMove() { + if (gameModel.isGameOver) { + showInfo(gameModel.getResultMessage()) + return + } + if (! isComputerToPlay()) + return + switch (gameModel.toPlay) { + case 0: if (! gameModel.hasMoves0) return; break + case 1: if (! gameModel.hasMoves1) return; break + case 2: if (! gameModel.hasMoves2) return; break + case 3: if (! gameModel.hasMoves3) return; break + } + genMove(); +} + +/** If the computer already plays the current color to play, start generating + a move; if he doesn't, make him play the current color (and only the + current color). */ +function computerPlay() { + if (playerModel.isGenMoveRunning) + return + if (! isComputerToPlay()) { + computerPlays0 = false + computerPlays1 = false + computerPlays2 = false + computerPlays3 = false + var variant = gameModel.gameVariant + if (variant == "classic_3" && gameModel.toPlay == 3) { + switch (gameModel.altPlayer) { + case 0: computerPlays0 = true; break + case 1: computerPlays1 = true; break + case 2: computerPlays2 = true; break + } + } + else + { + var isMultiColor = + (variant == "classic_2" || variant == "trigon_2" + || variant == "nexos_2") + switch (gameModel.toPlay) { + case 0: + computerPlays0 = true + if (isMultiColor) computerPlays2 = true + break; + case 1: + computerPlays1 = true + if (isMultiColor) computerPlays3 = true + break; + case 2: + computerPlays2 = true + if (isMultiColor) computerPlays0 = true + break; + case 3: + computerPlays3 = true + if (isMultiColor) computerPlays1 = true + break; + } + } + } + checkComputerMove() +} + +function computerPlays(color) { + switch (color) { + case 0: return computerPlays0 + case 1: return computerPlays1 + case 2: return computerPlays2 + case 3: return computerPlays3 + } +} + +function createTheme(themeName) { + var source = "qrc:///qml/themes/" + themeName + "/Theme.qml" + return Qt.createComponent(source).createObject(root) +} + +function deleteAllVar() { + showQuestion(qsTr("Delete all variations?"), gameModel.deleteAllVar) +} + +function genMove() { + gameDisplay.pickedPiece = null + isMoveHintRunning = false + playerModel.startGenMove(gameModel) +} + +function getFileFromUrl(fileUrl) { + var file = fileUrl.toString() + file = file.replace(/^(file:\/{3})/,"/") + return decodeURIComponent(file) +} + +function init() { + // Settings might contain unusable geometry + var maxWidth = Screen.desktopAvailableWidth + var maxHeight = Screen.desktopAvailableHeight + if (x < 0 || x + width > maxWidth || y < 0 || y + height > maxHeight) { + if (width > maxWidth || height > Screen.maxHeight) { + width = defaultWidth + height = defaultHeight + } + x = (maxWidth - width) / 2 + y = (maxHeight - height) / 2 + } + if (! gameModel.loadAutoSave()) { + gameDisplay.createPieces() + initComputerColors() + } + else { + gameDisplay.createPieces() + if (! gameModel.isGameOver) + checkComputerMove() + } +} + +function initComputerColors() { + // Default setting is that the computer plays all colors but the first + computerPlays0 = false + computerPlays1 = true + computerPlays2 = true + computerPlays3 = true + if (gameModel.gameVariant == "classic_2" + || gameModel.gameVariant == "trigon_2" + || gameModel.gameVariant == "nexos_2") + computerPlays2 = false +} + +function isComputerToPlay() { + if (gameModel.gameVariant == "classic_3" && gameModel.toPlay == 3) + return computerPlays(gameModel.altPlayer) + return computerPlays(gameModel.toPlay) +} + +function moveGenerated(move) { + if (isMoveHintRunning) { + gameDisplay.showMoveHint(move) + isMoveHintRunning = false + return + } + gameModel.playMove(move) + delayedCheckComputerMove.restart() +} + +function moveHint() { + if (gameModel.isGameOver) + return + isMoveHintRunning = true + playerModel.startGenMoveAtLevel(gameModel, 1) +} + +function newGameNoVerify() +{ + gameModel.newGame() + gameDisplay.showToPlay() + initComputerColors() +} + +function newGame() +{ + if (! gameModel.isGameEmpty && ! gameModel.isGameOver) { + showQuestion(qsTr("New game?"), newGameNoVerify) + return + } + newGameNoVerify() +} + +function openFileUrl() { + gameDisplay.destroyPieces() + if (! gameModel.open(getFileFromUrl(openDialog.item.fileUrl))) + showError(qsTr("Open failed.") + "\n" + gameModel.lastInputOutputError) + else { + computerPlays0 = false + computerPlays1 = false + computerPlays2 = false + computerPlays3 = false + } + gameDisplay.createPieces() + gameDisplay.showToPlay() +} + +function play(pieceModel, gameCoord) { + var wasComputerToPlay = isComputerToPlay() + gameModel.playPiece(pieceModel, gameCoord) + // We don't continue automatic play if the human played a move for a color + // played by the computer. + if (! wasComputerToPlay) + delayedCheckComputerMove.restart() +} + +function saveFileUrl(fileUrl) { + if (! gameModel.save(getFileFromUrl(fileUrl))) + showError(qsTr("Save failed.") + "\n" + gameModel.lastInputOutputError) +} + +function showComputerColorDialog() { + if (computerColorDialogLoader.status === Loader.Null) + computerColorDialogLoader.sourceComponent = + computerColorDialogComponent + var dialog = computerColorDialogLoader.item + dialog.computerPlays0 = computerPlays0 + dialog.computerPlays1 = computerPlays1 + dialog.computerPlays2 = computerPlays2 + dialog.computerPlays3 = computerPlays3 + dialog.open() +} + +function showError(text) { + if (errorMessageLoader.status === Loader.Null) + errorMessageLoader.sourceComponent = errorMessageComponent + var dialog = errorMessageLoader.item + dialog.text = text + dialog.open() +} + +function showInfo(text) { + if (infoMessageLoader.status === Loader.Null) + infoMessageLoader.sourceComponent = infoMessageComponent + var dialog = infoMessageLoader.item + dialog.text = text + dialog.open() +} + +function showQuestion(text, acceptedFunc) { + if (questionMessageLoader.status === Loader.Null) + questionMessageLoader.sourceComponent = questionMessageComponent + var dialog = questionMessageLoader.item + dialog.text = text + dialog.accepted.connect(acceptedFunc) + dialog.open() +} + +function truncate() { + showQuestion(qsTr("Truncate this subtree?"), gameModel.truncate) +} + +function truncateChildren() { + showQuestion(qsTr("Truncate children?"), gameModel.truncateChildren) +} + +function undo() { + gameModel.undo() +} diff --git a/src/pentobi_qml/qml/Main.qml b/src/pentobi_qml/qml/Main.qml new file mode 100644 index 0000000..a90ba50 --- /dev/null +++ b/src/pentobi_qml/qml/Main.qml @@ -0,0 +1,234 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.1 +import Qt.labs.settings 1.0 +import pentobi 1.0 +import "." as Pentobi +import "Main.js" as Logic + +ApplicationWindow { + id: root + + property bool computerPlays0 + property bool computerPlays1 + property bool computerPlays2 + property bool computerPlays3 + property bool isMoveHintRunning + property bool isAndroid: Qt.platform.os === "android" + property string themeName: isAndroid ? "dark" : "light" + property QtObject theme: Logic.createTheme(themeName) + property url folder + property int defaultWidth: + isAndroid ? Screen.desktopAvailableWidth : + Math.min(Screen.desktopAvailableWidth, + Math.round(Screen.pixelDensity / 3.5 * 600)) + property int defaultHeight: + isAndroid ? Screen.desktopAvailableWidth : + Math.min(Math.round(Screen.pixelDensity / 3.5 * 800)) + + function cancelGenMove() { + playerModel.cancelGenMove() + delayedCheckComputerMove.stop() + } + + minimumWidth: 240; minimumHeight: 320 + width: isAndroid ? Screen.desktopAvailableWidth : defaultWidth + height: isAndroid ? Screen.desktopAvailableHeight : defaultHeight + color: theme.backgroundColor + title: qsTr("Pentobi") + onClosing: Qt.quit() + // Currently, we don't use the QtQuick ToolBar/MenuBar on Android. The file + // dialog is unusable with dark themes (QTBUG-48324) and a white toolbar is + // too distracting with the dark background we use on Android. + menuBar: menuBarLoader.item + toolBar: toolBarLoader.item + Component.onCompleted: { + Logic.init() + show() + } + Component.onDestruction: gameModel.autoSave() + + ColumnLayout { + anchors.fill: parent + Keys.onReleased: if (isAndroid && event.key === Qt.Key_Menu) { + androidToolBarLoader.item.popupMenu() + event.accepted = true + } + + Loader { + id: androidToolBarLoader + + sourceComponent: isAndroid ? androidToolBarComponent : undefined + Layout.fillWidth: true + + Component { + id: androidToolBarComponent + + AndroidToolBar { } + } + } + GameDisplay { + id: gameDisplay + + busyIndicatorRunning: pieces0 === undefined + || lengthyCommand.isRunning + || playerModel.isGenMoveRunning + Layout.fillWidth: true + Layout.fillHeight: true + focus: true + onPlay: Logic.play(pieceModel, gameCoord) + } + } + Loader { + id: menuBarLoader + + sourceComponent: isAndroid ? undefined : menuBarComponent + + Component { + id: menuBarComponent + + MenuBar { + MenuGame { } + MenuGo { } + MenuEdit { } + MenuComputer { } + MenuView { } + MenuHelp { } + } + } + } + Loader { + id: toolBarLoader + + sourceComponent: isAndroid ? undefined : toolBarComponent + + Component { + id: toolBarComponent + + Pentobi.ToolBar { } + } + } + Settings { + id: settings + + property alias x: root.x + property alias y: root.y + property alias width: root.width + property alias height: root.height + property alias folder: root.folder + property alias enableAnimations: gameDisplay.enableAnimations + property alias markLastMove: gameDisplay.markLastMove + property alias computerPlays0: root.computerPlays0 + property alias computerPlays1: root.computerPlays1 + property alias computerPlays2: root.computerPlays2 + property alias computerPlays3: root.computerPlays3 + } + GameModel { + id: gameModel + + onPositionAboutToChange: cancelGenMove() + } + PlayerModel { + id: playerModel + + onMoveGenerated: Logic.moveGenerated(move) + } + Loader { id: computerColorDialogLoader } + Component { + id: computerColorDialogComponent + + ComputerColorDialog { + id: computerColorDialog + + gameVariant: gameModel.gameVariant + onAccepted: { + root.computerPlays0 = computerColorDialog.computerPlays0 + root.computerPlays1 = computerColorDialog.computerPlays1 + root.computerPlays2 = computerColorDialog.computerPlays2 + root.computerPlays3 = computerColorDialog.computerPlays3 + if (! Logic.isComputerToPlay()) + cancelGenMove() + else if (! gameModel.isGameOver) + Logic.checkComputerMove() + gameDisplay.forceActiveFocus() // QTBUG-48456 + } + onRejected: gameDisplay.forceActiveFocus() // QTBUG-48456 + } + } + Loader { + id: openDialog + + function open() { + if (status === Loader.Null) + setSource("OpenDialog.qml") + item.open() + } + } + Loader { + id: saveDialog + + function open() { + if (status === Loader.Null) + source = "SaveDialog.qml" + item.open() + } + } + Loader { id: errorMessageLoader } + Component { + id: errorMessageComponent + + MessageDialog { + icon: StandardIcon.Critical + } + } + Loader { id: infoMessageLoader } + Component { + id: infoMessageComponent + + MessageDialog { } + } + Loader { id: questionMessageLoader } + Component { + id: questionMessageComponent + + MessageDialog { + standardButtons: StandardButton.Ok | StandardButton.Cancel + } + } + // Used to delay calls to Logic.checkComputerMove such that the computer + // starts thinking and the busy indicator is visible after the current move + // placement animation has finished + Timer { + id: delayedCheckComputerMove + + interval: 400 + onTriggered: Logic.checkComputerMove() + } + // Delay lengthy function calls such that the busy indicator is visible + Timer { + id: lengthyCommand + + property bool isRunning + property var func + + function run(func) { + lengthyCommand.func = func + isRunning = true + restart() + } + + interval: 400 + onTriggered: { + func() + isRunning = false + } + } + Connections { + target: Qt.application + onStateChanged: + if (Qt.application.state === Qt.ApplicationSuspended) + gameModel.autoSave() + } +} diff --git a/src/pentobi_qml/qml/MenuComputer.qml b/src/pentobi_qml/qml/MenuComputer.qml new file mode 100644 index 0000000..6299b2b --- /dev/null +++ b/src/pentobi_qml/qml/MenuComputer.qml @@ -0,0 +1,47 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +Menu { + title: qsTr("&Computer") + + MenuItem { + text: qsTr("Computer &Colors") + visible: ! isAndroid + onTriggered: Logic.showComputerColorDialog() + } + MenuItem { + text: qsTr("&Play") + enabled: ! gameModel.isGameOver + visible: ! isAndroid + onTriggered: Logic.computerPlay() + } + Menu { + title: + switch (gameModel.gameVariant) + { + case "classic": return qsTr("&Level (Classic, 4 Players)") + case "classic_2": return qsTr("&Level (Classic, 2 Players)") + case "classic_3": return qsTr("&Level (Classic, 3 Players)") + case "duo": return qsTr("&Level (Duo)") + case "junior": return qsTr("&Level (Junior)") + case "trigon": return qsTr("&Level (Trigon, 4 Players)") + case "trigon_2": return qsTr("&Level (Trigon, 2 Players)") + case "trigon_3": return qsTr("&Level (Trigon, 3 Players)") + case "nexos": return qsTr("&Level (Nexos, 4 Players)") + case "nexos_2": return qsTr("&Level (Nexos, 2 Players)") + case "callisto": return qsTr("&Level (Callisto, 4 Players)") + case "callisto_2": return qsTr("&Level (Callisto, 2 Players)") + case "callisto_3": return qsTr("&Level (Callisto, 3 Players)") + } + + ExclusiveGroup { id: levelGroup } + MenuItemLevel { level: 1 } + MenuItemLevel { level: 2 } + MenuItemLevel { level: 3 } + MenuItemLevel { level: 4 } + MenuItemLevel { level: 5 } + MenuItemLevel { level: 6 } + MenuItemLevel { level: 7 } + } +} diff --git a/src/pentobi_qml/qml/MenuEdit.qml b/src/pentobi_qml/qml/MenuEdit.qml new file mode 100644 index 0000000..cacfaa1 --- /dev/null +++ b/src/pentobi_qml/qml/MenuEdit.qml @@ -0,0 +1,50 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +Menu { + title: qsTr("&Edit") + + MenuItem { + text: qsTr("Make &Main Variation") + enabled: ! gameModel.isMainVar + visible: ! isAndroid || enabled + onTriggered: gameModel.makeMainVar() + } + MenuItem { + text: qsTr("Move Variation &Up") + enabled: gameModel.hasPrevVar + visible: ! isAndroid || enabled + onTriggered: gameModel.moveUpVar() + } + MenuItem { + text: qsTr("Move Variation &Down") + enabled: gameModel.hasNextVar + visible: ! isAndroid || enabled + onTriggered: gameModel.moveDownVar() + } + MenuSeparator { } + MenuItem { + text: qsTr("&Delete All Variations") + enabled: gameModel.hasVariations + visible: ! isAndroid || enabled + onTriggered: Logic.deleteAllVar() + } + MenuItem { + text: qsTr("&Truncate") + enabled: gameModel.canGoBackward + visible: ! isAndroid || enabled + onTriggered: Logic.truncate() + } + MenuItem { + text: qsTr("Truncate &Children") + enabled: gameModel.canGoForward + visible: ! isAndroid || enabled + onTriggered: Logic.truncateChildren() + } + MenuSeparator { } + MenuItem { + text: qsTr("&Next Color") + onTriggered: gameModel.nextColor() + } +} diff --git a/src/pentobi_qml/qml/MenuGame.qml b/src/pentobi_qml/qml/MenuGame.qml new file mode 100644 index 0000000..7ebd92e --- /dev/null +++ b/src/pentobi_qml/qml/MenuGame.qml @@ -0,0 +1,119 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +Menu { + title: qsTr("&Game") + + MenuItem { + text: qsTr("&New") + enabled: ! gameModel.isGameEmpty + visible: ! isAndroid + onTriggered: Logic.newGame() + } + MenuSeparator { + visible: ! isAndroid + } + Menu { + title: qsTr("Game &Variant") + + ExclusiveGroup { id: groupGameVariant } + Menu { + title: qsTr("&Classic") + + MenuItemGameVariant { + gameVariant: "classic_2" + text: qsTr("Classic (&2 Players)") + } + MenuItemGameVariant { + gameVariant: "classic_3" + text: qsTr("Classic (&3 Players)") + } + MenuItemGameVariant { + gameVariant: "classic" + text: qsTr("Classic (&4 Players)") + } + } + MenuItemGameVariant { + gameVariant: "duo" + text: qsTr("&Duo") + } + MenuItemGameVariant { + gameVariant: "junior" + text: qsTr("&Junior") + } + Menu { + title: qsTr("&Trigon") + + MenuItemGameVariant { + gameVariant: "trigon_2" + text: qsTr("Trigon (&2 Players)") + } + MenuItemGameVariant { + gameVariant: "trigon_3" + text: qsTr("Trigon (&3 Players)") + } + MenuItemGameVariant { + gameVariant: "trigon" + text: qsTr("Trigon (&4 Players)") + } + } + Menu { + title: qsTr("&Nexos") + + MenuItemGameVariant { + gameVariant: "nexos_2" + text: qsTr("Nexos (&2 Players)") + } + MenuItemGameVariant { + gameVariant: "nexos" + text: qsTr("Nexos (&4 Players)") + } + } + Menu { + title: qsTr("C&allisto") + + MenuItemGameVariant { + gameVariant: "callisto_2" + text: qsTr("Callisto (&2 Players)") + } + MenuItemGameVariant { + gameVariant: "callisto_3" + text: qsTr("Callisto (&3 Players)") + } + MenuItemGameVariant { + gameVariant: "callisto" + text: qsTr("Callisto (&4 Players)") + } + } + } + MenuSeparator { } + MenuItem { + text: qsTr("&Undo Move") + enabled: gameModel.canUndo + visible: ! isAndroid + onTriggered: Logic.undo() + } + MenuItem { + text: qsTr("&Find Move") + enabled: ! gameModel.isGameOver + visible: ! isAndroid || enabled + onTriggered: Logic.moveHint() + } + MenuSeparator { } + MenuItem { + text: qsTr("&Open...") + onTriggered: openDialog.open() + } + MenuItem { + text: qsTr("&Save As...") + enabled: ! gameModel.isGameEmpty + visible: ! isAndroid || enabled + onTriggered: saveDialog.open() + } + MenuSeparator { } + MenuItem { + text: qsTr("&Quit") + onTriggered: Qt.quit() + } +} diff --git a/src/pentobi_qml/qml/MenuGo.qml b/src/pentobi_qml/qml/MenuGo.qml new file mode 100644 index 0000000..253ff0f --- /dev/null +++ b/src/pentobi_qml/qml/MenuGo.qml @@ -0,0 +1,17 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +Menu { + title: qsTr("G&o") + visible: ! isAndroid || backToMainVar.enabled + + MenuItem { + id: backToMainVar + + text: qsTr("Back to &Main Variation") + enabled: ! gameModel.isMainVar + visible: ! isAndroid || enabled + onTriggered: gameModel.backToMainVar() + } +} diff --git a/src/pentobi_qml/qml/MenuHelp.qml b/src/pentobi_qml/qml/MenuHelp.qml new file mode 100644 index 0000000..d26ea34 --- /dev/null +++ b/src/pentobi_qml/qml/MenuHelp.qml @@ -0,0 +1,12 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +Menu { + title: qsTr("&Help") + + MenuItem { + text: qsTr("&About Pentobi") + onTriggered: Logic.about() + } +} diff --git a/src/pentobi_qml/qml/MenuItemGameVariant.qml b/src/pentobi_qml/qml/MenuItemGameVariant.qml new file mode 100644 index 0000000..6999084 --- /dev/null +++ b/src/pentobi_qml/qml/MenuItemGameVariant.qml @@ -0,0 +1,12 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +MenuItem { + property string gameVariant + + checkable: true + checked: gameModel.gameVariant == gameVariant + exclusiveGroup: groupGameVariant + onTriggered: Logic.changeGameVariant(gameVariant) +} diff --git a/src/pentobi_qml/qml/MenuItemLevel.qml b/src/pentobi_qml/qml/MenuItemLevel.qml new file mode 100644 index 0000000..6c3c9c1 --- /dev/null +++ b/src/pentobi_qml/qml/MenuItemLevel.qml @@ -0,0 +1,43 @@ +import QtQuick.Controls 1.1 + +MenuItem { + property int level + + text: "&" + level + checkable: true + exclusiveGroup: levelGroup + checked: { + switch (gameModel.gameVariant) { + case "classic_2": return playerModel.levelClassic2 === level + case "classic_3": return playerModel.levelClassic3 === level + case "duo": return playerModel.levelDuo === level + case "trigon": return playerModel.levelTrigon === level + case "trigon_2": return playerModel.levelTrigon2 === level + case "trigon_3": return playerModel.levelTrigon3 === level + case "junior": return playerModel.levelJunior === level + case "nexos": return playerModel.levelNexos === level + case "nexos_2": return playerModel.levelNexos2 === level + case "callisto": return playerModel.levelCallisto === level + case "callisto_2": return playerModel.levelCallisto2 === level + case "callisto_3": return playerModel.levelCallisto3 === level + default: return playerModel.levelClassic === level + } + } + onTriggered: { + switch (gameModel.gameVariant) { + case "classic_2": playerModel.levelClassic2 = level; break + case "classic_3": playerModel.levelClassic3 = level; break + case "duo": playerModel.levelDuo = level; break + case "trigon": playerModel.levelTrigon = level; break + case "trigon_2": playerModel.levelTrigon2 = level; break + case "trigon_3": playerModel.levelTrigon3 = level; break + case "junior": playerModel.levelJunior = level; break + case "nexos": playerModel.levelNexos = level; break + case "nexos_2": playerModel.levelNexos2 = level; break + case "callisto": playerModel.levelCallisto = level; break + case "callisto_2": playerModel.levelCallisto2 = level; break + case "callisto_3": playerModel.levelCallisto3 = level; break + default: playerModel.levelClassic = level + } + } +} diff --git a/src/pentobi_qml/qml/MenuView.qml b/src/pentobi_qml/qml/MenuView.qml new file mode 100644 index 0000000..cec07f5 --- /dev/null +++ b/src/pentobi_qml/qml/MenuView.qml @@ -0,0 +1,19 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 + +Menu { + title: qsTr("&View") + + MenuItem { + text: qsTr("Mark &Last Move") + checkable: true + checked: gameDisplay.markLastMove + onTriggered: gameDisplay.markLastMove = checked + } + MenuItem { + text: qsTr("&Animate Pieces") + checkable: true + checked: gameDisplay.enableAnimations + onTriggered: gameDisplay.enableAnimations = checked + } +} diff --git a/src/pentobi_qml/qml/NavigationPanel.qml b/src/pentobi_qml/qml/NavigationPanel.qml new file mode 100644 index 0000000..4db4d53 --- /dev/null +++ b/src/pentobi_qml/qml/NavigationPanel.qml @@ -0,0 +1,57 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.0 +import "." as Pentobi + +ColumnLayout { + id: root + + Text { + text: gameModel.positionInfo + color: theme.fontColorPosInfo + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } + RowLayout + { + width: root.width; height: width / 6 + + Pentobi.Button { + enabled: gameModel.canGoBackward + imageSource: "icons/pentobi-beginning.svg" + Layout.fillWidth: true + onClicked: gameModel.goBeginning() + } + Pentobi.Button { + enabled: gameModel.canGoBackward + imageSource: "icons/pentobi-backward.svg" + Layout.fillWidth: true + onClicked: gameModel.goBackward() + autoRepeat: true + } + Pentobi.Button { + enabled: gameModel.canGoForward + imageSource: "icons/pentobi-forward.svg" + Layout.fillWidth: true + onClicked: gameModel.goForward() + autoRepeat: true + } + Pentobi.Button { + enabled: gameModel.canGoForward + imageSource: "icons/pentobi-end.svg" + Layout.fillWidth: true + onClicked: gameModel.goEnd() + } + Pentobi.Button { + enabled: gameModel.hasPrevVar + imageSource: "icons/pentobi-previous-variation.svg" + Layout.fillWidth: true + onClicked: gameModel.goPrevVar() + } + Pentobi.Button { + enabled: gameModel.hasNextVar + imageSource: "icons/pentobi-next-variation.svg" + Layout.fillWidth: true + onClicked: gameModel.goNextVar() + } + } +} diff --git a/src/pentobi_qml/qml/OpenDialog.qml b/src/pentobi_qml/qml/OpenDialog.qml new file mode 100644 index 0000000..c9076c7 --- /dev/null +++ b/src/pentobi_qml/qml/OpenDialog.qml @@ -0,0 +1,15 @@ +import QtQuick 2.0 +import QtQuick.Dialogs 1.2 +import "Main.js" as Logic + +FileDialog { + title: qsTr("Open") + nameFilters: [ qsTr("Blokus games (*.blksgf)"), qsTr("All files (*)") ] + folder: root.folder == "" ? shortcuts.desktop : root.folder + onAccepted: { + root.folder = folder + gameDisplay.forceActiveFocus() // QTBUG-48456 + lengthyCommand.run(Logic.openFileUrl) + } + onRejected: gameDisplay.forceActiveFocus() // QTBUG-48456 +} diff --git a/src/pentobi_qml/qml/PieceCallisto.qml b/src/pentobi_qml/qml/PieceCallisto.qml new file mode 100644 index 0000000..9f635d8 --- /dev/null +++ b/src/pentobi_qml/qml/PieceCallisto.qml @@ -0,0 +1,293 @@ +import QtQuick 2.3 + +// See PieceClassic.qml for comments +Item +{ + id: root + + property var pieceModel + property string colorName + property bool isPicked + property Item parentUnplayed + property real gridWidth: board.gridWidth + property real gridHeight: board.gridHeight + property bool isMarked + property string imageName: pieceModel.elements.length === 1 ? + theme.getImage("frame-" + colorName) : + theme.getImage("square-" + colorName) + property real pieceAngle: { + var flX = Math.abs(flipX.angle % 360 - 180) < 90 + var flY = Math.abs(flipY.angle % 360 - 180) < 90 + var angle = rotation + if (flX && flY) angle += 180 + else if (flX) angle += 90 + else if (flY) angle += 270 + return angle + } + property real imageOpacity0: imageOpacity(pieceAngle, 0) + property real imageOpacity90: imageOpacity(pieceAngle, 90) + property real imageOpacity180: imageOpacity(pieceAngle, 180) + property real imageOpacity270: imageOpacity(pieceAngle, 270) + + z: 1 + transform: [ + Rotation { + id: flipX + + axis { x: 1; y: 0; z: 0 } + origin { x: width / 2; y: height / 2 } + }, + Rotation { + id: flipY + + axis { x: 0; y: 1; z: 0 } + origin { x: width / 2; y: height / 2 } + } + ] + + function imageOpacity(pieceAngle, imgAngle) { + var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360 + return (angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180)) + } + + Repeater { + model: pieceModel.elements + + Item { + Square { + width: 0.9 * gridWidth + height: 0.9 * gridHeight + x: (modelData.x - pieceModel.center.x) * gridWidth + + (gridWidth - width) / 2 + y: (modelData.y - pieceModel.center.y) * gridHeight + + (gridHeight - height) / 2 + } + // Right junction + Image { + visible: pieceModel.junctionType[index] === 0 + || pieceModel.junctionType[index] === 1 + source: theme.getImage("junction-all-" + colorName) + width: 0.1 * gridWidth + height: 0.85 * gridHeight + x: (modelData.x - pieceModel.center.x + 1) * gridWidth + - width / 2 + y: (modelData.y - pieceModel.center.y) * gridHeight + + (gridHeight - height) / 2 + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + } + // Down junction + Image { + visible: pieceModel.junctionType[index] === 0 + || pieceModel.junctionType[index] === 2 + source: theme.getImage("junction-all-" + colorName) + width: 0.85 * gridWidth + height: 0.1 * gridHeight + x: (modelData.x - pieceModel.center.x) * gridWidth + + (gridWidth - width) / 2 + y: (modelData.y - pieceModel.center.y + 1) * gridHeight + - height / 2 + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + } + } + } + Rectangle { + opacity: isMarked ? 0.5 : 0 + color: colorName == "blue" || colorName == "red" + || pieceModel.elements.length === 1 ? "white" : "#333333" + width: 0.3 * gridHeight + height: width + radius: width / 2 + x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5) + * gridWidth - width / 2 + y: (pieceModel.labelPos.y - pieceModel.center.y + 0.5) + * gridHeight - height / 2 + Behavior on opacity { NumberAnimation { duration: 80 } } + } + StateGroup { + state: pieceModel.state + + states: [ + State { + name: "rot90" + PropertyChanges { target: root; rotation: 90 } + }, + State { + name: "rot180" + PropertyChanges { target: root; rotation: 180 } + }, + State { + name: "rot270" + PropertyChanges { target: root; rotation: 270 } + }, + State { + name: "flip" + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot90Flip" + PropertyChanges { target: root; rotation: 90 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot180Flip" + PropertyChanges { target: root; rotation: 180 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot270Flip" + PropertyChanges { target: root; rotation: 270 } + PropertyChanges { target: flipX; angle: 180 } + } + ] + + transitions: [ + Transition { + from: ",rot90,rot180,rot270"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: "flip,rot90Flip,rot180Flip,rot270Flip"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: ",flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot90,rot90Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot180,rot180Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot270,rot270Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: ",rot180Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot90,rot270Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot180,flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot270,rot90Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + } + ] + } + + states: [ + State { + name: "picked" + when: isPicked + + ParentChange { + target: root + parent: pieceManipulator + x: pieceManipulator.width / 2 + y: pieceManipulator.height / 2 + } + }, + State { + name: "played" + when: pieceModel.isPlayed + + ParentChange { + target: root + parent: board + x: board.mapFromGameX(pieceModel.gameCoord.x) + y: board.mapFromGameY(pieceModel.gameCoord.y) + } + }, + State { + name: "unplayed" + when: parentUnplayed != null + + PropertyChanges { + target: root + // Avoid fractional sizes for square piece elements + scale: Math.floor(0.25 * parentUnplayed.width) / gridWidth + } + ParentChange { + target: root + parent: parentUnplayed + x: parentUnplayed.width / 2 + y: parentUnplayed.height / 2 + } + } + ] + transitions: + Transition { + from: "unplayed,picked,played"; to: from + enabled: enableAnimations + + ParentAnimation { + via: gameDisplay + NumberAnimation { + properties: "x,y,scale" + duration: 300 + easing.type: Easing.InOutQuad + } + } + } +} diff --git a/src/pentobi_qml/qml/PieceClassic.qml b/src/pentobi_qml/qml/PieceClassic.qml new file mode 100644 index 0000000..afa9041 --- /dev/null +++ b/src/pentobi_qml/qml/PieceClassic.qml @@ -0,0 +1,259 @@ +import QtQuick 2.0 + +Item +{ + id: root + + property var pieceModel + property string colorName + property bool isPicked + property Item parentUnplayed + property real gridWidth: board.gridWidth + property real gridHeight: board.gridHeight + property bool isMarked + property string imageName: theme.getImage("square-" + colorName) + property real pieceAngle: { + var flX = Math.abs(flipX.angle % 360 - 180) < 90 + var flY = Math.abs(flipY.angle % 360 - 180) < 90 + var angle = rotation + if (flX && flY) angle += 180 + else if (flX) angle += 90 + else if (flY) angle += 270 + return angle + } + property real imageOpacity0: imageOpacity(pieceAngle, 0) + property real imageOpacity90: imageOpacity(pieceAngle, 90) + property real imageOpacity180: imageOpacity(pieceAngle, 180) + property real imageOpacity270: imageOpacity(pieceAngle, 270) + + z: 1 // Must be above board and piece manipulator during transition + transform: [ + Rotation { + id: flipX + + axis { x: 1; y: 0; z: 0 } + origin { x: width / 2; y: height / 2 } + }, + Rotation { + id: flipY + + axis { x: 0; y: 1; z: 0 } + origin { x: width / 2; y: height / 2 } + } + ] + + function imageOpacity(pieceAngle, imgAngle) { + var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360 // JS modulo bug + return (angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180)) + } + + Repeater { + model: pieceModel.elements + + Square { + width: gridWidth + height: gridHeight + x: (modelData.x - pieceModel.center.x) * gridWidth + y: (modelData.y - pieceModel.center.y) * gridHeight + } + } + Rectangle { + opacity: isMarked ? 0.5 : 0 + color: colorName == "blue" || colorName == "red" ? + "white" : "#333333" + width: 0.3 * gridHeight + height: width + radius: width / 2 + x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5) + * gridWidth - width / 2 + y: (pieceModel.labelPos.y - pieceModel.center.y + 0.5) + * gridHeight - height / 2 + Behavior on opacity { NumberAnimation { duration: 80 } } + } + StateGroup { + state: pieceModel.state + + states: [ + State { + name: "rot90" + PropertyChanges { target: root; rotation: 90 } + }, + State { + name: "rot180" + PropertyChanges { target: root; rotation: 180 } + }, + State { + name: "rot270" + PropertyChanges { target: root; rotation: 270 } + }, + State { + name: "flip" + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot90Flip" + PropertyChanges { target: root; rotation: 90 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot180Flip" + PropertyChanges { target: root; rotation: 180 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot270Flip" + PropertyChanges { target: root; rotation: 270 } + PropertyChanges { target: flipX; angle: 180 } + } + ] + + // Unique states are defined by rotating and flipping around the x axis + // but for some transitions, the shortest visual animation is flipping + // around the y axis. + transitions: [ + Transition { + from: ",rot90,rot180,rot270"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: "flip,rot90Flip,rot180Flip,rot270Flip"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: ",flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot90,rot90Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot180,rot180Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot270,rot270Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: ",rot180Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot90,rot270Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot180,flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot270,rot90Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + } + ] + } + + states: [ + State { + name: "picked" + when: isPicked + + ParentChange { + target: root + parent: pieceManipulator + x: pieceManipulator.width / 2 + y: pieceManipulator.height / 2 + } + }, + State { + name: "played" + when: pieceModel.isPlayed + + ParentChange { + target: root + parent: board + x: board.mapFromGameX(pieceModel.gameCoord.x) + y: board.mapFromGameY(pieceModel.gameCoord.y) + } + }, + State { + name: "unplayed" + when: parentUnplayed != null + + PropertyChanges { + target: root + // Avoid fractional sizes for square piece elements + scale : Math.floor(0.2 * parentUnplayed.width) / gridWidth + } + ParentChange { + target: root + parent: parentUnplayed + x: parentUnplayed.width / 2 + y: parentUnplayed.height / 2 + } + } + ] + transitions: + Transition { + from: "unplayed,picked,played"; to: from + enabled: enableAnimations + + ParentAnimation { + via: gameDisplay + NumberAnimation { + properties: "x,y,scale" + duration: 300 + easing.type: Easing.InOutQuad + } + } + } +} diff --git a/src/pentobi_qml/qml/PieceFlipAnimation.qml b/src/pentobi_qml/qml/PieceFlipAnimation.qml new file mode 100644 index 0000000..9c43deb --- /dev/null +++ b/src/pentobi_qml/qml/PieceFlipAnimation.qml @@ -0,0 +1,7 @@ +import QtQuick 2.0 + +RotationAnimation { + duration: 300 + direction: RotationAnimation.Shortest + property: "angle" +} diff --git a/src/pentobi_qml/qml/PieceList.qml b/src/pentobi_qml/qml/PieceList.qml new file mode 100644 index 0000000..4caa196 --- /dev/null +++ b/src/pentobi_qml/qml/PieceList.qml @@ -0,0 +1,26 @@ +import QtQuick 2.0 + +Grid { + id: root + + property var pieces + + signal piecePicked(var piece) + + opacity: theme.pieceListOpacity + + Repeater { + model: pieces + + MouseArea { + id: mouseArea + + property var piece: modelData + + width: root.width / columns; height: width + visible: ! piece.pieceModel.isPlayed + onClicked: piecePicked(piece) + Component.onCompleted: piece.parentUnplayed = mouseArea + } + } +} diff --git a/src/pentobi_qml/qml/PieceManipulator.qml b/src/pentobi_qml/qml/PieceManipulator.qml new file mode 100644 index 0000000..9a06b5b --- /dev/null +++ b/src/pentobi_qml/qml/PieceManipulator.qml @@ -0,0 +1,72 @@ +import QtQuick 2.0 + +Item { + id: root + + property var pieceModel + // True if piece manipulator is at a board location that is a legal move + property bool legal + + signal piecePlayed + + Image { + anchors.fill: root + source: theme.getImage("piece-manipulator") + sourceSize { width: width; height: height } + opacity: ! legal ? 0.4 : 0 + Behavior on opacity { NumberAnimation { duration: 100 } } + } + Image { + anchors.fill: root + source: theme.getImage("piece-manipulator-legal") + sourceSize { width: width; height: height } + opacity: legal ? 0.4 : 0 + Behavior on opacity { NumberAnimation { duration: 100 } } + } + MouseArea { + id: dragArea + + anchors.fill: root + drag { + target: root + filterChildren: true + minimumX: -width / 2; maximumX: root.parent.width - width / 2 + minimumY: -height / 2; maximumY: root.parent.height - height / 2 + } + + MouseArea { + anchors.centerIn: dragArea + width: 0.5 * root.width; height: width + onClicked: piecePlayed() + } + MouseArea { + anchors { + top: dragArea.top + horizontalCenter: dragArea.horizontalCenter + } + width: 0.2 * root.width; height: width + onClicked: pieceModel.rotateRight() + } + MouseArea { + anchors { + right: dragArea.right + verticalCenter: dragArea.verticalCenter + } + width: 0.2 * root.width; height: width + onClicked: pieceModel.flipAcrossX() + } + MouseArea { + anchors { + bottom: dragArea.bottom + horizontalCenter: dragArea.horizontalCenter + } + width: 0.2 * root.width; height: width + onClicked: pieceModel.flipAcrossY() + } + MouseArea { + anchors { left: dragArea.left; verticalCenter: dragArea.verticalCenter } + width: 0.2 * root.width; height: width + onClicked: pieceModel.rotateLeft() + } + } +} diff --git a/src/pentobi_qml/qml/PieceNexos.qml b/src/pentobi_qml/qml/PieceNexos.qml new file mode 100644 index 0000000..b5b7d0a --- /dev/null +++ b/src/pentobi_qml/qml/PieceNexos.qml @@ -0,0 +1,310 @@ +import QtQuick 2.3 + +// Piece for Nexos. See PieceClassic for comments. +Item +{ + id: root + + property var pieceModel + property string colorName + property bool isPicked + property Item parentUnplayed + property real gridWidth: board.gridWidth + property real gridHeight: board.gridHeight + property bool isMarked + property string imageName: theme.getImage("linesegment-" + colorName) + property real pieceAngle: { + var flX = Math.abs(flipX.angle % 360 - 180) < 90 + var flY = Math.abs(flipY.angle % 360 - 180) < 90 + var angle = rotation + if (flX && flY) angle += 180 + else if (flX) angle += 90 + else if (flY) angle += 270 + return angle + } + property real imageOpacity0: imageOpacity(pieceAngle, 0) + property real imageOpacity90: imageOpacity(pieceAngle, 90) + property real imageOpacity180: imageOpacity(pieceAngle, 180) + property real imageOpacity270: imageOpacity(pieceAngle, 270) + + z: 1 + transform: [ + Rotation { + id: flipX + + axis { x: 1; y: 0; z: 0 } + origin { x: width / 2; y: height / 2 } + }, + Rotation { + id: flipY + + axis { x: 0; y: 1; z: 0 } + origin { x: width / 2; y: height / 2 } + } + ] + + function isHorizontal(pos) { return (pos.x % 2 != 0) } + function imageOpacity(pieceAngle, imgAngle) { + var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360 + return (angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180)) + } + + Repeater { + model: pieceModel.elements + + LineSegment { + isHorizontal: root.isHorizontal(modelData) + width: 1.5 * gridWidth + height: 0.5 * gridHeight + x: (modelData.x - pieceModel.center.x - 0.25) * gridWidth + y: (modelData.y - pieceModel.center.y + 0.25) * gridHeight + } + } + Repeater { + model: pieceModel.junctions + + Image { + source: { + switch (pieceModel.junctionType[index]) { + case 0: + return theme.getImage("junction-all-" + colorName) + case 1: + case 2: + case 3: + case 4: + return theme.getImage("junction-t-" + colorName) + case 5: + case 6: + return theme.getImage("junction-straight-" + colorName) + case 7: + case 8: + case 9: + case 10: + return theme.getImage("junction-rect-" + colorName) + } + } + rotation: { + switch (pieceModel.junctionType[index]) { + case 0: + case 3: + case 5: + case 10: + return 0 + case 1: + case 9: + return 270 + case 2: + case 6: + case 8: + return 90 + case 4: + case 7: + return 180 + } + } + width: 0.5 * gridWidth + height: 0.5 * gridHeight + x: (modelData.x - pieceModel.center.x + 0.25) * gridWidth + y: (modelData.y - pieceModel.center.y + 0.25) * gridHeight + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + } + } + Rectangle { + opacity: isMarked ? 0.5 : 0 + color: colorName == "blue" || colorName == "red" ? + "white" : "#333333" + width: 0.3 * gridHeight + height: width + radius: width / 2 + x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5) + * gridWidth - width / 2 + y: (pieceModel.labelPos.y - pieceModel.center.y + 0.5) + * gridHeight - height / 2 + Behavior on opacity { NumberAnimation { duration: 80 } } + } + StateGroup { + state: pieceModel.state + + states: [ + State { + name: "rot90" + PropertyChanges { target: root; rotation: 90 } + }, + State { + name: "rot180" + PropertyChanges { target: root; rotation: 180 } + }, + State { + name: "rot270" + PropertyChanges { target: root; rotation: 270 } + }, + State { + name: "flip" + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot90Flip" + PropertyChanges { target: root; rotation: 90 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot180Flip" + PropertyChanges { target: root; rotation: 180 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot270Flip" + PropertyChanges { target: root; rotation: 270 } + PropertyChanges { target: flipX; angle: 180 } + } + ] + + transitions: [ + Transition { + from: ",rot90,rot180,rot270"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: "flip,rot90Flip,rot180Flip,rot270Flip"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: ",flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot90,rot90Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot180,rot180Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot270,rot270Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: ",rot180Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot90,rot270Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot180,flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot270,rot90Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + } + ] + } + + states: [ + State { + name: "picked" + when: isPicked + + ParentChange { + target: root + parent: pieceManipulator + x: pieceManipulator.width / 2 + y: pieceManipulator.height / 2 + } + }, + State { + name: "played" + when: pieceModel.isPlayed + + ParentChange { + target: root + parent: board + x: board.mapFromGameX(pieceModel.gameCoord.x) + y: board.mapFromGameY(pieceModel.gameCoord.y) + } + }, + State { + name: "unplayed" + when: parentUnplayed != null + + PropertyChanges { + target: root + // Avoid fractional sizes for square piece elements + scale: Math.floor(0.12 * parentUnplayed.width) / gridWidth + } + ParentChange { + target: root + parent: parentUnplayed + x: parentUnplayed.width / 2 + y: parentUnplayed.height / 2 + } + } + ] + transitions: + Transition { + from: "unplayed,picked,played"; to: from + enabled: enableAnimations + + ParentAnimation { + via: gameDisplay + NumberAnimation { + properties: "x,y,scale" + duration: 300 + easing.type: Easing.InOutQuad + } + } + } +} diff --git a/src/pentobi_qml/qml/PieceRotationAnimation.qml b/src/pentobi_qml/qml/PieceRotationAnimation.qml new file mode 100644 index 0000000..0f2c268 --- /dev/null +++ b/src/pentobi_qml/qml/PieceRotationAnimation.qml @@ -0,0 +1,7 @@ +import QtQuick 2.0 + +RotationAnimation { + duration: 300 + direction: RotationAnimation.Shortest + property: "rotation" +} diff --git a/src/pentobi_qml/qml/PieceSelector.qml b/src/pentobi_qml/qml/PieceSelector.qml new file mode 100644 index 0000000..30c9f66 --- /dev/null +++ b/src/pentobi_qml/qml/PieceSelector.qml @@ -0,0 +1,195 @@ +import QtQuick 2.0 + +Flickable { + id: root + + property string gameVariant + property int toPlay + property var pieces0 + property var pieces1 + property var pieces2 + property var pieces3 + property int nuColors + property int columns + property int rows + property bool transitionsEnabled + + signal piecePicked(var piece) + + contentHeight: pieceList0.height + pieceList1.height + + pieceList2.height + pieceList3.height + flickableDirection: Flickable.VerticalFlick + clip: true + + PieceList { + id: pieceList0 + + width: root.width + columns: root.columns + pieces: pieces0 + onPiecePicked: root.piecePicked(piece) + } + PieceList { + id: pieceList1 + + width: root.width + columns: root.columns + pieces: pieces1 + onPiecePicked: root.piecePicked(piece) + } + PieceList { + id: pieceList2 + + width: root.width + columns: root.columns + pieces: pieces2 + onPiecePicked: root.piecePicked(piece) + } + PieceList { + id: pieceList3 + + width: root.width + columns: root.columns + pieces: pieces3 + onPiecePicked: root.piecePicked(piece) + } + + states: [ + State { + name: "toPlay0" + when: toPlay === 0 + + PropertyChanges { + target: pieceList0 + y: 0 + } + PropertyChanges { + target: pieceList1 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList0.height : + pieceList0.height + pieceList2.height + } + PropertyChanges { + target: pieceList2 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList0.height + pieceList1.height : + pieceList0.height + } + PropertyChanges { + target: pieceList3 + y: pieceList0.height + pieceList1.height + pieceList2.height + } + }, + State { + name: "toPlay1" + when: toPlay === 1 + + PropertyChanges { + target: pieceList1 + y: 0 + } + PropertyChanges { + target: pieceList2 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList1.height : + pieceList1.height + pieceList3.height + } + PropertyChanges { + target: pieceList3 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList1.height + pieceList2.height : + pieceList1.height + } + PropertyChanges { + target: pieceList0 + y: pieceList1.height + pieceList2.height + pieceList3.height + } + }, + State { + name: "toPlay2" + when: toPlay === 2 + + PropertyChanges { + target: pieceList2 + y: 0 + } + PropertyChanges { + target: pieceList3 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList2.height : + pieceList2.height + pieceList0.height + } + PropertyChanges { + target: pieceList0 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList2.height + pieceList3.height : + pieceList2.height + } + PropertyChanges { + target: pieceList1 + y: pieceList2.height + pieceList3.height + pieceList0.height + } + }, + State { + name: "toPlay3" + when: toPlay === 3 + + PropertyChanges { + target: pieceList3 + y: 0 + } + PropertyChanges { + target: pieceList0 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList3.height : + pieceList3.height + pieceList1.height + } + PropertyChanges { + target: pieceList1 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList3.height + pieceList0.height : + pieceList3.height + } + PropertyChanges { + target: pieceList2 + y: pieceList3.height + pieceList0.height + pieceList1.height + } + } + ] + transitions: + Transition { + enabled: transitionsEnabled + + SequentialAnimation { + PropertyAction { + target: pieceList0; property: "y"; value: pieceList0.y } + PropertyAction { + target: pieceList1; property: "y"; value: pieceList1.y } + PropertyAction { + target: pieceList2; property: "y"; value: pieceList2.y } + PropertyAction { + target: pieceList3; property: "y"; value: pieceList3.y } + // Delay showing new color because of piece placement animation + PauseAnimation { duration: 200 } + NumberAnimation { + target: root; property: "opacity"; to: 0; duration: 100 + } + PropertyAction { target: pieceList0; property: "y" } + PropertyAction { target: pieceList1; property: "y" } + PropertyAction { target: pieceList2; property: "y" } + PropertyAction { target: pieceList3; property: "y" } + PropertyAction { target: root; property: "contentY"; value: 0 } + NumberAnimation { + target: root; property: "opacity"; to: 1; duration: 100 + } + } + } +} diff --git a/src/pentobi_qml/qml/PieceTrigon.qml b/src/pentobi_qml/qml/PieceTrigon.qml new file mode 100644 index 0000000..91436ea --- /dev/null +++ b/src/pentobi_qml/qml/PieceTrigon.qml @@ -0,0 +1,320 @@ +import QtQuick 2.0 + +// See PieceClassic.qml for comments +Item +{ + id: root + + property var pieceModel + property string colorName + property bool isPicked + property Item parentUnplayed + property real gridWidth: board.gridWidth + property real gridHeight: board.gridHeight + property bool isMarked + property string imageName: theme.getImage("triangle-" + colorName) + property string imageNameDownward: + theme.getImage("triangle-down-" + colorName) + property real pieceAngle: { + var flX = Math.abs(flipX.angle % 360 - 180) < 90 + var flY = Math.abs(flipY.angle % 360 - 180) < 90 + var angle = rotation + if (flX && flY) angle += 180 + else if (flX) angle += 120 + else if (flY) angle += 300 + return angle + } + property real imageOpacity0: imageOpacity(pieceAngle, 0) + property real imageOpacity60: imageOpacity(pieceAngle, 60) + property real imageOpacity120: imageOpacity(pieceAngle, 120) + property real imageOpacity180: imageOpacity(pieceAngle, 180) + property real imageOpacity240: imageOpacity(pieceAngle, 240) + property real imageOpacity300: imageOpacity(pieceAngle, 300) + + z: 1 + transform: [ + Rotation { + id: flipX + + axis { x: 1; y: 0; z: 0 } + origin { x: width / 2; y: height / 2 } + }, + Rotation { + id: flipY + + axis { x: 0; y: 1; z: 0 } + origin { x: width / 2; y: height / 2 } + } + ] + + function _isDownward(pos) { return (pos.x % 2 == 0) != (pos.y % 2 == 0) } + function imageOpacity(pieceAngle, imgAngle) { + var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360 + return (angle >= 60 && angle <= 300 ? 0 : 2 * Math.cos(angle * Math.PI / 180) - 1) + } + + Repeater { + model: pieceModel.elements + + Triangle { + isDownward: _isDownward(modelData) + width: 2 * gridWidth + height: gridHeight + x: (modelData.x - pieceModel.center.x - 0.5) * gridWidth + y: (modelData.y - pieceModel.center.y) * gridHeight + } + } + Rectangle { + opacity: isMarked ? 0.5 : 0 + color: colorName == "blue" || colorName == "red" ? + "white" : "#333333" + width: 0.3 * gridHeight + height: width + radius: width / 2 + x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5) + * gridWidth - width / 2 + y: (pieceModel.labelPos.y - pieceModel.center.y + + (_isDownward(pieceModel.labelPos) ? 1 : 2) / 3) + * gridHeight - height / 2 + Behavior on opacity { NumberAnimation { duration: 80 } } + } + StateGroup { + state: pieceModel.state + + states: [ + State { + name: "rot60" + PropertyChanges { target: root; rotation: 60 } + }, + State { + name: "rot120" + PropertyChanges { target: root; rotation: 120 } + }, + State { + name: "rot180" + PropertyChanges { target: root; rotation: 180 } + }, + State { + name: "rot240" + PropertyChanges { target: root; rotation: 240 } + }, + State { + name: "rot300" + PropertyChanges { target: root; rotation: 300 } + }, + State { + name: "flip" + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot60Flip" + PropertyChanges { target: root; rotation: 60 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot120Flip" + PropertyChanges { target: root; rotation: 120 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot180Flip" + PropertyChanges { target: root; rotation: 180 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot240Flip" + PropertyChanges { target: root; rotation: 240 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot300Flip" + PropertyChanges { target: root; rotation: 300 } + PropertyChanges { target: flipX; angle: 180 } + } + ] + + transitions: [ + Transition { + from: ",rot60,rot120,rot180,rot240,rot300"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: "flip,rot60Flip,rot120Flip,rot180Flip,rot240Flip,rot300Flip"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: ",flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot60,rot60Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot120,rot120Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot180,rot180Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot240,rot240Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot300,rot300Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: ",rot180Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot60,rot240Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot120,rot300Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot180,flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot240,rot60Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot300,rot120Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + } + ] + } + + states: [ + State { + name: "picked" + when: isPicked + + ParentChange { + target: root + parent: pieceManipulator + x: pieceManipulator.width / 2 + y: pieceManipulator.height / 2 + } + }, + State { + name: "played" + when: pieceModel.isPlayed + + ParentChange { + target: root + parent: board + x: board.mapFromGameX(pieceModel.gameCoord.x) + y: board.mapFromGameY(pieceModel.gameCoord.y) + } + }, + State { + name: "unplayed" + when: parentUnplayed != null + + PropertyChanges { + target: root + scale: 0.13 * parentUnplayed.width / gridWidth + } + ParentChange { + target: root + parent: parentUnplayed + x: parentUnplayed.width / 2 + y: parentUnplayed.height / 2 + } + } + ] + + transitions: + Transition { + from: "unplayed,picked,played"; to: from + enabled: enableAnimations + + ParentAnimation { + via: gameDisplay + NumberAnimation { + properties: "x,y,scale" + duration: 300 + easing.type: Easing.InOutQuad + } + } + } +} diff --git a/src/pentobi_qml/qml/SaveDialog.qml b/src/pentobi_qml/qml/SaveDialog.qml new file mode 100644 index 0000000..1a59687 --- /dev/null +++ b/src/pentobi_qml/qml/SaveDialog.qml @@ -0,0 +1,16 @@ +import QtQuick 2.0 +import QtQuick.Dialogs 1.2 +import "Main.js" as Logic + +FileDialog { + title: qsTr("Save") + selectExisting: false + folder: root.folder == "" ? shortcuts.desktop : root.folder + nameFilters: [ qsTr("Blokus games (*.blksgf)"), qsTr("All files (*)") ] + onAccepted: { + Logic.saveFileUrl(fileUrl) + root.folder = folder + gameDisplay.forceActiveFocus() // QTBUG-48456 + } + onRejected: gameDisplay.forceActiveFocus() // QTBUG-48456 +} diff --git a/src/pentobi_qml/qml/ScoreDisplay.qml b/src/pentobi_qml/qml/ScoreDisplay.qml new file mode 100644 index 0000000..7e842a1 --- /dev/null +++ b/src/pentobi_qml/qml/ScoreDisplay.qml @@ -0,0 +1,110 @@ +import QtQuick 2.0 + +Row { + id: root + + property real pointSize + property int toPlay + property int altPlayer + property string gameVariant + property real points0 + property real points1 + property real points2 + property real points3 + property real bonus0 + property real bonus1 + property real bonus2 + property real bonus3 + property bool hasMoves0 + property bool hasMoves1 + property bool hasMoves2 + property bool hasMoves3 + + ScoreElement2 { + visible: gameVariant == "classic_2" || gameVariant == "trigon_2" + || gameVariant == "nexos_2" + value: points0 + points2 + isFinal: ! hasMoves0 && ! hasMoves2 + pointSize: root.pointSize + height: root.height + width: 5.9 * pointSize + color1: theme.colorBlue + color2: theme.colorRed + } + ScoreElement2 { + visible: gameVariant == "classic_2" || gameVariant == "trigon_2" + || gameVariant == "nexos_2" + value: points1 + points3 + isFinal: ! hasMoves1 && ! hasMoves3 + pointSize: root.pointSize + height: root.height + width: 5.9 * pointSize + color1: theme.colorYellow + color2: theme.colorGreen + } + ScoreElement { + value: points0 + bonus: bonus0 + isFinal: ! hasMoves0 + isToPlay: toPlay == 0 + pointSize: root.pointSize + height: root.height + width: 5 * pointSize + color: theme.colorBlue + } + ScoreElement { + value: points1 + bonus: bonus1 + isFinal: ! hasMoves1 + isToPlay: toPlay == 1 + pointSize: root.pointSize + height: root.height + width: 5 * pointSize + color: gameModel.gameVariant == "duo" + || gameModel.gameVariant == "junior" + || gameModel.gameVariant == "callisto_2" ? + theme.colorGreen : theme.colorYellow + } + ScoreElement { + visible: gameVariant != "duo" && gameVariant != "junior" + && gameVariant != "callisto_2" + value: points2 + bonus: bonus2 + isFinal: ! hasMoves2 + isToPlay: toPlay == 2 + pointSize: root.pointSize + height: root.height + width: 5 * pointSize + color: theme.colorRed + } + ScoreElement { + visible: gameVariant != "duo" && gameVariant != "junior" + && gameVariant != "callisto_2" && gameVariant != "trigon_3" + && gameVariant != "classic_3" && gameVariant != "callisto_3" + value: points3 + bonus: bonus3 + isFinal: ! hasMoves3 + isToPlay: toPlay == 3 + pointSize: root.pointSize + height: root.height + width: 5 * pointSize + color: theme.colorGreen + } + ScoreElement2 { + visible: gameVariant == "classic_3" + value: points3 + isAltColor: true + isToPlay: toPlay == 3 + isFinal: ! hasMoves3 + pointSize: root.pointSize + height: root.height + width: 5.9 * pointSize + color1: theme.colorGreen + color2: + switch (altPlayer) { + case 0: return theme.colorBlue + case 1: return theme.colorYellow + case 2: return theme.colorRed + } + } +} diff --git a/src/pentobi_qml/qml/ScoreElement.qml b/src/pentobi_qml/qml/ScoreElement.qml new file mode 100644 index 0000000..1211cf4 --- /dev/null +++ b/src/pentobi_qml/qml/ScoreElement.qml @@ -0,0 +1,40 @@ +import QtQuick 2.0 + +Item { + id: root + + property alias color: point.color + property bool isFinal + property bool isToPlay + property real value + property real bonus + property real pointSize + + Rectangle { + id: point + + width: (isToPlay ? 1.3 : 1) * pointSize + border { + color: Qt.lighter(color, theme.toPlayColorLighter) + width: isToPlay ? Math.max(0.15 * pointSize, 1) : 0 + } + height: width + radius: width / 2 + anchors.verticalCenter: root.verticalCenter + } + Text { + id: scoreText + + text: ! isFinal ? + value : (bonus > 0 ? "*" : "") + "" + value + "" + color: theme.fontColorScore + anchors { + left: point.right + leftMargin: (isToPlay ? 0.2 : 0.4) * point.width + verticalCenter: root.verticalCenter + } + verticalAlignment: Text.AlignVCenter + renderType: Text.NativeRendering + font.pixelSize: 1.4 * pointSize + } +} diff --git a/src/pentobi_qml/qml/ScoreElement2.qml b/src/pentobi_qml/qml/ScoreElement2.qml new file mode 100644 index 0000000..436c15a --- /dev/null +++ b/src/pentobi_qml/qml/ScoreElement2.qml @@ -0,0 +1,58 @@ +import QtQuick 2.0 + +Item { + id: root + + property color color1 + property color color2 + property bool isFinal + property bool isToPlay + property bool isAltColor + property real value + property real pointSize + + Rectangle { + id: point1 + + color: color1 + opacity: isAltColor && isFinal ? 0 : 1 + width: (isToPlay ? 1.3 : 1) * pointSize + border { + color: Qt.lighter(color1, theme.toPlayColorLighter) + width: isToPlay ? Math.max(0.15 * pointSize, 1) : 0 + } + height: width + radius: width / 2 + anchors.verticalCenter: root.verticalCenter + } + Rectangle { + id: point2 + + color: isAltColor && isFinal ? color1 : color2 + width: pointSize + height: width + radius: width / 2 + anchors { + left: point1.right + verticalCenter: root.verticalCenter + } + } + Text { + text: { + if (isAltColor) + return isFinal ? "(" + value + ")" : "(" + value + ")" + else + return isFinal ? "" + value + "" : value + } + color: theme.fontColorScore + width: root.width - point1.width - point2.width - anchors.leftMargin + anchors { + left: point2.right + leftMargin: (isToPlay ? 0.2 : 0.4) * point1.width + verticalCenter: root.verticalCenter + } + verticalAlignment: Text.AlignVCenter + renderType: Text.NativeRendering + font.pixelSize: 1.4 * pointSize + } +} diff --git a/src/pentobi_qml/qml/Square.qml b/src/pentobi_qml/qml/Square.qml new file mode 100644 index 0000000..a2901ec --- /dev/null +++ b/src/pentobi_qml/qml/Square.qml @@ -0,0 +1,100 @@ +import QtQuick 2.3 + +// Piece element (square) with pseudo-3D effect. +// Simulates lighting by using different images for the lighting at different +// rotations and interpolating between them with an opacity animation. +Item { + id: root + + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component0 + } + + anchors.fill: root + opacity: imageOpacity0 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component0 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component90 + } + + anchors.fill: root + opacity: imageOpacity90 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component90 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + rotation: -90 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component180 + } + + anchors.fill: root + opacity: imageOpacity180 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component180 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + rotation: -180 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component270 + } + + anchors.fill: root + opacity: imageOpacity270 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component270 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + rotation: -270 + } + } + } +} diff --git a/src/pentobi_qml/qml/ToolBar.qml b/src/pentobi_qml/qml/ToolBar.qml new file mode 100644 index 0000000..d478f6a --- /dev/null +++ b/src/pentobi_qml/qml/ToolBar.qml @@ -0,0 +1,30 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import QtQuick.Layouts 1.1 +import "Main.js" as Logic + +ToolBar { + RowLayout { + anchors.fill: parent + + ToolButton { + iconSource: "icons/pentobi-newgame.svg" + enabled: ! gameModel.isGameEmpty + onClicked: Logic.newGame() + } + ToolButton { + iconSource: "icons/pentobi-undo.svg" + enabled: gameModel.canUndo + onClicked: Logic.undo() + } + ToolButton { + iconSource: "icons/pentobi-computer-colors.svg" + onClicked: Logic.showComputerColorDialog() + } + ToolButton { + iconSource: "icons/pentobi-play.svg" + onClicked: Logic.computerPlay() + } + Item { Layout.fillWidth: true } + } +} diff --git a/src/pentobi_qml/qml/Triangle.qml b/src/pentobi_qml/qml/Triangle.qml new file mode 100644 index 0000000..79083c8 --- /dev/null +++ b/src/pentobi_qml/qml/Triangle.qml @@ -0,0 +1,176 @@ +import QtQuick 2.3 + +// See Square.qml for comments +Item { + id: root + + property bool isDownward + + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component0 + } + + anchors.fill: root + opacity: imageOpacity0 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component0 + + Image { + source: isDownward ? imageNameDownward : imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component60 + } + + anchors.fill: root + opacity: imageOpacity60 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component60 + + Image { + source: isDownward ? imageName : imageNameDownward + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + transform: [ + Rotation { + angle: -60 + origin { + x: width / 2 + y: isDownward ? 2 * height / 3 : height / 3 + } + }, + Translate { y: isDownward ? -height / 3 : height / 3 } + ] + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component120 + } + + anchors.fill: root + opacity: imageOpacity120 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component120 + + Image { + source: isDownward ? imageNameDownward : imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + transform: Rotation { + angle: -120 + origin { + x: width / 2 + y: isDownward ? height / 3 : 2 * height / 3 + } + } + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component180 + } + + anchors.fill: root + opacity: imageOpacity180 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component180 + + Image { + source: isDownward ? imageName : imageNameDownward + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + rotation: -180 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component240 + } + + anchors.fill: root + opacity: imageOpacity240 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component240 + + Image { + source: isDownward ? imageNameDownward : imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + transform: Rotation { + angle: -240 + origin { + x: width / 2 + y: isDownward ? height / 3 : 2 * height / 3 + } + } + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component300 + } + + anchors.fill: root + opacity: imageOpacity300 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component300 + + Image { + source: isDownward ? imageName : imageNameDownward + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + transform: [ + Rotation { + angle: -300 + origin { + x: width / 2 + y: isDownward ? 2 * height / 3 : height / 3 + } + }, + Translate { y: isDownward ? -height / 3 : height / 3 } + ] + } + } + } +} diff --git a/src/pentobi_qml/qml/i18n/qml_de.ts b/src/pentobi_qml/qml/i18n/qml_de.ts new file mode 100644 index 0000000..9e7af56 --- /dev/null +++ b/src/pentobi_qml/qml/i18n/qml_de.ts @@ -0,0 +1,480 @@ + + + + + ComputerColorDialog + + Computer Colors + Computer-Farben + + + Computer plays: + Computer spielt: + + + Blue/Red + Blau/Rot + + + Blue + Blau + + + Yellow/Green + Gelb/Grün + + + Green + Grün + + + Yellow + Gelb + + + Red + Rot + + + + GameModel + + (Setup) + (Stellung) + + + (No moves) + (Keine Züge) + + + Move %1 + Zug %1 + + + Blue wins with 1 point. + Blau gewinnt mit 1 Punkt. + + + Blue wins with %1 points. + Blau gewinnt mit %1 Punkten. + + + Green wins with 1 point. + Grün gewinnt mit 1 Punkt. + + + Green wins with %1 points. + Grün gewinnt mit %1 Punkten. + + + Green wins (tie resolved). + Grün gewinnt (Unentschieden aufgelöst). + + + Game ends in a tie. + Spiel endet unentschieden. + + + Blue/Red wins with 1 point. + Blau/Rot gewinnt mit 1 Punkt. + + + Blue/Red wins with %1 points. + Blau/Rot gewinnt mit %1 Punkten. + + + Yellow/Green wins with 1 point. + Gelb/Grün gewinnt mit 1 Punkt. + + + Yellow/Green wins with %1 points. + Gelb/Grün gewinnt mit %1 Punkten. + + + Yellow/Green wins (tie resolved). + Gelb/Grün gewinnt (Unentschieden aufgelöst). + + + Blue wins. + Blau gewinnt. + + + Yellow wins. + Gelb gewinnt. + + + Red wins. + Rot gewinnt. + + + Red wins (tie resolved). + Rot gewinnt (Unentschieden aufgelöst). + + + Yellow wins (tie resolved). + Gelb gewinnt (Unentschieden aufgelöst). + + + Game ends in a tie between Blue and Yellow. + Spiel endet in einem Unentschieden zwischen Blau und Gelb. + + + Game ends in a tie between Blue and Red. + Spiel endet in einem Unentschieden zwischen Blau und Rot. + + + Game ends in a tie between Yellow and Red. + Spiel endet in einem Unentschieden zwischen Gelb und Rot. + + + Game ends in a tie between all players. + Spiel endet in einem Unentschieden zwischen allen Spielern. + + + Green wins. + Grün gewinnt. + + + Game ends in a tie between Blue, Yellow and Red. + Spiel endet in einem Unentschieden zwischen Blau, Gelb und Rot. + + + Game ends in a tie between Blue, Yellow and Green. + Spiel endet in einem Unentschieden zwischen Blau, Gelb und Grün. + + + Game ends in a tie between Blue, Red and Green. + Spiel endet in einem Unentschieden zwischen Blau, Rot und Grün. + + + Game ends in a tie between Yellow, Red and Green. + Spiel endet in einem Unentschieden zwischen Gelb, Rot und Grün. + + + + Main + + Pentobi + Pentobi + + + New game? + Neues Spiel? + + + Open failed. + Öffnen fehlgeschlagen. + + + Save failed. + Speichern fehlgeschlagen. + + + Truncate this subtree? + Diesen Teilbaum abschneiden? + + + Truncate children? + Kindknoten abschneiden? + + + Delete all variations? + Alle Varianten löschen? + + + Version %1 + Version %1 + + + Computer opponent for the board game Blokus. + Computer-Gegner für das Brettspiel Blokus. + + + &copy; 2011&ndash;%1 Markus&nbsp;Enzenberger + &copy; 2011&ndash;%1 Markus&nbsp;Enzenberger + + + + MenuComputer + + &Computer + &Computer + + + Computer &Colors + Computer-&Farben + + + &Play + &Spielen + + + &Level (Classic, 4 Players) + Spielst&ufe (Klassisch, 4 Spieler) + + + &Level (Classic, 2 Players) + Spielst&ufe (Klassisch, 2 Spieler) + + + &Level (Classic, 3 Players) + Spielst&ufe (Klassisch, 3 Spieler) + + + &Level (Duo) + Spielst&ufe (Duo) + + + &Level (Junior) + Spielst&ufe (Junior) + + + &Level (Trigon, 4 Players) + Spielst&ufe (Trigon, 4 Spieler) + + + &Level (Trigon, 2 Players) + Spielst&ufe (Trigon, 2 Spieler) + + + &Level (Trigon, 3 Players) + Spielst&ufe (Trigon, 3 Spieler) + + + &Level (Nexos, 4 Players) + Spielst&ufe (Nexos, 4 Spieler) + + + &Level (Nexos, 2 Players) + Spielst&ufe (Nexos, 2 Spieler) + + + &Level (Callisto, 4 Players) + Spielst&ufe (Callisto, 4 Spieler) + + + &Level (Callisto, 2 Players) + Spielst&ufe (Callisto, 2 Spieler) + + + &Level (Callisto, 3 Players) + Spielst&ufe (Callisto, 3 Spieler) + + + + MenuEdit + + &Edit + &Bearbeiten + + + Make &Main Variation + Zu &Hauptvariante machen + + + Move Variation &Up + Variante nach &oben schieben + + + Move Variation &Down + Variante nach &unten schieben + + + &Truncate + &Abschneiden + + + Truncate &Children + &Kindknoten abschneiden + + + &Delete All Variations + Alle &Varianten löschen + + + &Next Color + &Nächste Farbe + + + + MenuGame + + &Game + &Spiel + + + &New + &Neu + + + Game &Variant + Spiel&variante + + + Classic (&3 Players) + Klassisch (&3 Spieler) + + + Classic (&4 Players) + Klassisch (&4 Spieler) + + + &Duo + &Duo + + + &Junior + &Junior + + + &Undo Move + Zug &rückgängig + + + &Find Move + Zug &finden + + + &Open... + Öffn&en ... + + + &Save As... + &Speichern unter ... + + + &Quit + &Beenden + + + &Classic + &Klassisch + + + Classic (&2 Players) + Klassisch (&2 Spieler) + + + &Trigon + &Trigon + + + Trigon (&2 Players) + Trigon (&2 Spieler) + + + Trigon (&3 Players) + Trigon (&3 Spieler) + + + Trigon (&4 Players) + Trigon (&4 Spieler) + + + &Nexos + &Nexos + + + Nexos (&2 Players) + Nexos (&2 Spieler) + + + Nexos (&4 Players) + Nexos (&4 Spieler) + + + C&allisto + &Callisto + + + Callisto (&2 Players) + Callisto (&2 Spieler) + + + Callisto (&3 Players) + Callisto (&3 Spieler) + + + Callisto (&4 Players) + Callisto (&4 Spieler) + + + + MenuGo + + G&o + &Gehe zu + + + Back to &Main Variation + Zurück zu &Hauptvariante + + + + MenuHelp + + &Help + &Hilfe + + + &About Pentobi + Über &Pentobi + + + + MenuView + + &View + &Ansicht + + + Mark &Last Move + &Letzten Zug markieren + + + &Animate Pieces + Spielsteine &animieren + + + + OpenDialog + + Open + Öffnen + + + Blokus games (*.blksgf) + Blokus-Partien (*.blksgf) + + + All files (*) + Alle Dateien (*) + + + + SaveDialog + + Save + Speichern + + + Blokus games (*.blksgf) + Blokus-Partien (*.blksgf) + + + All files (*) + Alle Dateien (*) + + + + main + + Not enough memory. + Nicht genügend Speicher. + + + Pentobi + Pentobi + + + diff --git a/src/pentobi_qml/qml/i18n/replace_qtbase_de.ts b/src/pentobi_qml/qml/i18n/replace_qtbase_de.ts new file mode 100644 index 0000000..0fd0c68 --- /dev/null +++ b/src/pentobi_qml/qml/i18n/replace_qtbase_de.ts @@ -0,0 +1,11 @@ + + + + + QPlatformTheme + + Cancel + Abbrechen + + + diff --git a/src/pentobi_qml/qml/icons/menu.svg b/src/pentobi_qml/qml/icons/menu.svg new file mode 100644 index 0000000..6aed026 --- /dev/null +++ b/src/pentobi_qml/qml/icons/menu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-backward.svg b/src/pentobi_qml/qml/icons/pentobi-backward.svg new file mode 100644 index 0000000..993c75d --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-backward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-beginning.svg b/src/pentobi_qml/qml/icons/pentobi-beginning.svg new file mode 100644 index 0000000..baf6c04 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-beginning.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-computer-colors.svg b/src/pentobi_qml/qml/icons/pentobi-computer-colors.svg new file mode 100644 index 0000000..8e7c890 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-computer-colors.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-end.svg b/src/pentobi_qml/qml/icons/pentobi-end.svg new file mode 100644 index 0000000..7b4349e --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-end.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-forward.svg b/src/pentobi_qml/qml/icons/pentobi-forward.svg new file mode 100644 index 0000000..1fe5404 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-forward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-newgame.svg b/src/pentobi_qml/qml/icons/pentobi-newgame.svg new file mode 100644 index 0000000..202a885 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-newgame.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-next-variation.svg b/src/pentobi_qml/qml/icons/pentobi-next-variation.svg new file mode 100644 index 0000000..5a75498 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-next-variation.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-play.svg b/src/pentobi_qml/qml/icons/pentobi-play.svg new file mode 100644 index 0000000..e5441ce --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-play.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-previous-variation.svg b/src/pentobi_qml/qml/icons/pentobi-previous-variation.svg new file mode 100644 index 0000000..a58d536 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-previous-variation.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-undo.svg b/src/pentobi_qml/qml/icons/pentobi-undo.svg new file mode 100644 index 0000000..1c23026 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-undo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/dark/Theme.qml b/src/pentobi_qml/qml/themes/dark/Theme.qml new file mode 100644 index 0000000..edaac53 --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/Theme.qml @@ -0,0 +1,26 @@ +import QtQuick 2.0 + +QtObject { + property color backgroundColor: "#131313" + property color fontColorScore: "#C8C1BE" + property color fontColorPosInfo: "#C8C1BE" + property color colorBlue: "#0077D2" + property color colorYellow: "#EBCD23" + property color colorRed: "#E63E2C" + property color colorGreen: "#00C000" + property color colorStartingPoint: "#82777E" + property color backgroundButtonPressed: Qt.lighter(backgroundColor, 3) + property real pieceListOpacity: 0.94 + property real toPlayColorLighter: 1.7 + + function getImage(name) { + if (name.lastIndexOf("frame-", 0) === 0 + || name.lastIndexOf("junction-", 0) === 0 + || name.lastIndexOf("linesegment-", 0) === 0 + || name.lastIndexOf("piece-manipulator", 0) === 0 + || name.lastIndexOf("square-", 0) === 0 + || name.lastIndexOf("triangle-", 0) === 0) + return "themes/light/" + name + ".svg" + return "themes/dark/" + name + ".svg" + } +} diff --git a/src/pentobi_qml/qml/themes/dark/board-callisto-2.svg b/src/pentobi_qml/qml/themes/dark/board-callisto-2.svg new file mode 100644 index 0000000..dedd14c --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-callisto-2.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-callisto-3.svg b/src/pentobi_qml/qml/themes/dark/board-callisto-3.svg new file mode 100644 index 0000000..b97a7f9 --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-callisto-3.svg @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-callisto.svg b/src/pentobi_qml/qml/themes/dark/board-callisto.svg new file mode 100644 index 0000000..17b22f3 --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-callisto.svg @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-tile-classic.svg b/src/pentobi_qml/qml/themes/dark/board-tile-classic.svg new file mode 100644 index 0000000..a216929 --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-tile-classic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-tile-nexos.svg b/src/pentobi_qml/qml/themes/dark/board-tile-nexos.svg new file mode 100644 index 0000000..6be4d0a --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-tile-nexos.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-trigon-3.svg b/src/pentobi_qml/qml/themes/dark/board-trigon-3.svg new file mode 100644 index 0000000..0964fde --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-trigon-3.svg @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-trigon.svg b/src/pentobi_qml/qml/themes/dark/board-trigon.svg new file mode 100644 index 0000000..ec0dbc3 --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-trigon.svg @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/Theme.qml b/src/pentobi_qml/qml/themes/light/Theme.qml new file mode 100644 index 0000000..0950ec6 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/Theme.qml @@ -0,0 +1,17 @@ +import QtQuick 2.0 + +QtObject { + property color backgroundColor: "#E6E5E5" + property color fontColorScore: "#5A5755" + property color fontColorPosInfo: "#282625" + property color colorBlue: "#0077D2" + property color colorYellow: "#EBCD23" + property color colorRed: "#E63E2C" + property color colorGreen: "#00C000" + property color colorStartingPoint: "#767074" + property color backgroundButtonPressed: Qt.lighter(backgroundColor) + property real pieceListOpacity: 1 + property real toPlayColorLighter: 0.5 + + function getImage(name) { return "themes/light/" + name + ".svg" } +} diff --git a/src/pentobi_qml/qml/themes/light/board-callisto-2.svg b/src/pentobi_qml/qml/themes/light/board-callisto-2.svg new file mode 100644 index 0000000..c7d2e5d --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-callisto-2.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-callisto-3.svg b/src/pentobi_qml/qml/themes/light/board-callisto-3.svg new file mode 100644 index 0000000..bb0aeb3 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-callisto-3.svg @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-callisto.svg b/src/pentobi_qml/qml/themes/light/board-callisto.svg new file mode 100644 index 0000000..4338a7c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-callisto.svg @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-tile-classic.svg b/src/pentobi_qml/qml/themes/light/board-tile-classic.svg new file mode 100644 index 0000000..31c0c82 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-tile-classic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-tile-nexos.svg b/src/pentobi_qml/qml/themes/light/board-tile-nexos.svg new file mode 100644 index 0000000..7d5d4df --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-tile-nexos.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-trigon-3.svg b/src/pentobi_qml/qml/themes/light/board-trigon-3.svg new file mode 100644 index 0000000..5450dc1 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-trigon-3.svg @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-trigon.svg b/src/pentobi_qml/qml/themes/light/board-trigon.svg new file mode 100644 index 0000000..d7964e1 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-trigon.svg @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/frame-blue.svg b/src/pentobi_qml/qml/themes/light/frame-blue.svg new file mode 100644 index 0000000..b55e32e --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/frame-blue.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/frame-green.svg b/src/pentobi_qml/qml/themes/light/frame-green.svg new file mode 100644 index 0000000..dae34d9 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/frame-green.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/frame-red.svg b/src/pentobi_qml/qml/themes/light/frame-red.svg new file mode 100644 index 0000000..b73327b --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/frame-red.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/frame-yellow.svg b/src/pentobi_qml/qml/themes/light/frame-yellow.svg new file mode 100644 index 0000000..948a459 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/frame-yellow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-all-blue.svg b/src/pentobi_qml/qml/themes/light/junction-all-blue.svg new file mode 100644 index 0000000..339e9d0 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-all-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-all-green.svg b/src/pentobi_qml/qml/themes/light/junction-all-green.svg new file mode 100644 index 0000000..02f2de2 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-all-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-all-red.svg b/src/pentobi_qml/qml/themes/light/junction-all-red.svg new file mode 100644 index 0000000..6f7b77c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-all-red.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-all-yellow.svg b/src/pentobi_qml/qml/themes/light/junction-all-yellow.svg new file mode 100644 index 0000000..52f3c58 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-all-yellow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-rect-blue.svg b/src/pentobi_qml/qml/themes/light/junction-rect-blue.svg new file mode 100644 index 0000000..74dea2c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-rect-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-rect-green.svg b/src/pentobi_qml/qml/themes/light/junction-rect-green.svg new file mode 100644 index 0000000..6b88d65 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-rect-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-rect-red.svg b/src/pentobi_qml/qml/themes/light/junction-rect-red.svg new file mode 100644 index 0000000..fdd2958 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-rect-red.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-rect-yellow.svg b/src/pentobi_qml/qml/themes/light/junction-rect-yellow.svg new file mode 100644 index 0000000..d13407c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-rect-yellow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-straight-blue.svg b/src/pentobi_qml/qml/themes/light/junction-straight-blue.svg new file mode 100644 index 0000000..05fce46 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-straight-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-straight-green.svg b/src/pentobi_qml/qml/themes/light/junction-straight-green.svg new file mode 100644 index 0000000..321ef53 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-straight-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-straight-red.svg b/src/pentobi_qml/qml/themes/light/junction-straight-red.svg new file mode 100644 index 0000000..78f4a3a --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-straight-red.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-straight-yellow.svg b/src/pentobi_qml/qml/themes/light/junction-straight-yellow.svg new file mode 100644 index 0000000..eb9a97c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-straight-yellow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-t-blue.svg b/src/pentobi_qml/qml/themes/light/junction-t-blue.svg new file mode 100644 index 0000000..3468515 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-t-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-t-green.svg b/src/pentobi_qml/qml/themes/light/junction-t-green.svg new file mode 100644 index 0000000..1f36933 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-t-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-t-red.svg b/src/pentobi_qml/qml/themes/light/junction-t-red.svg new file mode 100644 index 0000000..b9a1367 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-t-red.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-t-yellow.svg b/src/pentobi_qml/qml/themes/light/junction-t-yellow.svg new file mode 100644 index 0000000..cf431aa --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-t-yellow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/linesegment-blue.svg b/src/pentobi_qml/qml/themes/light/linesegment-blue.svg new file mode 100644 index 0000000..a4f9eb1 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/linesegment-blue.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/linesegment-green.svg b/src/pentobi_qml/qml/themes/light/linesegment-green.svg new file mode 100644 index 0000000..c8a0160 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/linesegment-green.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/linesegment-red.svg b/src/pentobi_qml/qml/themes/light/linesegment-red.svg new file mode 100644 index 0000000..102bee5 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/linesegment-red.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/linesegment-yellow.svg b/src/pentobi_qml/qml/themes/light/linesegment-yellow.svg new file mode 100644 index 0000000..548d2d8 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/linesegment-yellow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/piece-manipulator-legal.svg b/src/pentobi_qml/qml/themes/light/piece-manipulator-legal.svg new file mode 100644 index 0000000..05928bb --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/piece-manipulator-legal.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/piece-manipulator.svg b/src/pentobi_qml/qml/themes/light/piece-manipulator.svg new file mode 100644 index 0000000..187e09e --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/piece-manipulator.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/square-blue.svg b/src/pentobi_qml/qml/themes/light/square-blue.svg new file mode 100644 index 0000000..63ed8aa --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/square-blue.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/square-green.svg b/src/pentobi_qml/qml/themes/light/square-green.svg new file mode 100644 index 0000000..916e69c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/square-green.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/square-red.svg b/src/pentobi_qml/qml/themes/light/square-red.svg new file mode 100644 index 0000000..0e58a50 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/square-red.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/square-yellow.svg b/src/pentobi_qml/qml/themes/light/square-yellow.svg new file mode 100644 index 0000000..1989427 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/square-yellow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-blue.svg b/src/pentobi_qml/qml/themes/light/triangle-blue.svg new file mode 100644 index 0000000..7a82407 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-blue.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-down-blue.svg b/src/pentobi_qml/qml/themes/light/triangle-down-blue.svg new file mode 100644 index 0000000..d93a7e0 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-down-blue.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-down-green.svg b/src/pentobi_qml/qml/themes/light/triangle-down-green.svg new file mode 100644 index 0000000..d848f64 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-down-green.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-down-red.svg b/src/pentobi_qml/qml/themes/light/triangle-down-red.svg new file mode 100644 index 0000000..1b5f55b --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-down-red.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-down-yellow.svg b/src/pentobi_qml/qml/themes/light/triangle-down-yellow.svg new file mode 100644 index 0000000..403f542 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-down-yellow.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-green.svg b/src/pentobi_qml/qml/themes/light/triangle-green.svg new file mode 100644 index 0000000..198b7bb --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-green.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-red.svg b/src/pentobi_qml/qml/themes/light/triangle-red.svg new file mode 100644 index 0000000..1afa8da --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-red.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-yellow.svg b/src/pentobi_qml/qml/themes/light/triangle-yellow.svg new file mode 100644 index 0000000..bb2c535 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-yellow.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/theme_dark.qrc b/src/pentobi_qml/qml/themes/theme_dark.qrc new file mode 100644 index 0000000..08609ea --- /dev/null +++ b/src/pentobi_qml/qml/themes/theme_dark.qrc @@ -0,0 +1,12 @@ + + +dark/board-callisto.svg +dark/board-callisto-2.svg +dark/board-callisto-3.svg +dark/board-tile-classic.svg +dark/board-tile-nexos.svg +dark/board-trigon.svg +dark/board-trigon-3.svg +dark/Theme.qml + + diff --git a/src/pentobi_qml/qml/themes/theme_light.qrc b/src/pentobi_qml/qml/themes/theme_light.qrc new file mode 100644 index 0000000..f4706d8 --- /dev/null +++ b/src/pentobi_qml/qml/themes/theme_light.qrc @@ -0,0 +1,12 @@ + + +light/board-callisto.svg +light/board-callisto-2.svg +light/board-callisto-3.svg +light/board-tile-classic.svg +light/board-tile-nexos.svg +light/board-trigon.svg +light/board-trigon-3.svg +light/Theme.qml + + diff --git a/src/pentobi_qml/qml/themes/theme_shared.qrc b/src/pentobi_qml/qml/themes/theme_shared.qrc new file mode 100644 index 0000000..63f183b --- /dev/null +++ b/src/pentobi_qml/qml/themes/theme_shared.qrc @@ -0,0 +1,42 @@ + + +light/frame-blue.svg +light/frame-green.svg +light/frame-red.svg +light/frame-yellow.svg +light/junction-all-blue.svg +light/junction-all-green.svg +light/junction-all-red.svg +light/junction-all-yellow.svg +light/junction-rect-blue.svg +light/junction-rect-green.svg +light/junction-rect-red.svg +light/junction-rect-yellow.svg +light/junction-straight-blue.svg +light/junction-straight-green.svg +light/junction-straight-red.svg +light/junction-straight-yellow.svg +light/junction-t-blue.svg +light/junction-t-green.svg +light/junction-t-red.svg +light/junction-t-yellow.svg +light/linesegment-blue.svg +light/linesegment-green.svg +light/linesegment-red.svg +light/linesegment-yellow.svg +light/piece-manipulator-legal.svg +light/piece-manipulator.svg +light/square-blue.svg +light/square-green.svg +light/square-red.svg +light/square-yellow.svg +light/triangle-blue.svg +light/triangle-down-blue.svg +light/triangle-down-green.svg +light/triangle-down-red.svg +light/triangle-down-yellow.svg +light/triangle-green.svg +light/triangle-red.svg +light/triangle-yellow.svg + + diff --git a/src/pentobi_qml/resources.qrc b/src/pentobi_qml/resources.qrc new file mode 100644 index 0000000..db9c32c --- /dev/null +++ b/src/pentobi_qml/resources.qrc @@ -0,0 +1,40 @@ + + + qml/AndroidToolBar.qml + qml/AndroidToolButton.qml + qml/Board.qml + qml/Button.qml + qml/ComputerColorDialog.qml + qml/GameDisplay.qml + qml/LineSegment.qml + qml/Main.qml + qml/MenuComputer.qml + qml/MenuEdit.qml + qml/MenuGame.qml + qml/MenuGo.qml + qml/MenuHelp.qml + qml/MenuItemGameVariant.qml + qml/MenuItemLevel.qml + qml/MenuView.qml + qml/NavigationPanel.qml + qml/OpenDialog.qml + qml/PieceCallisto.qml + qml/PieceClassic.qml + qml/PieceFlipAnimation.qml + qml/PieceList.qml + qml/PieceManipulator.qml + qml/PieceNexos.qml + qml/PieceRotationAnimation.qml + qml/PieceSelector.qml + qml/PieceTrigon.qml + qml/SaveDialog.qml + qml/ScoreDisplay.qml + qml/ScoreElement.qml + qml/ScoreElement2.qml + qml/Square.qml + qml/ToolBar.qml + qml/Triangle.qml + qml/Main.js + qml/GameDisplay.js + + diff --git a/src/pentobi_qml/translations.qrc b/src/pentobi_qml/translations.qrc new file mode 100644 index 0000000..9cc6b55 --- /dev/null +++ b/src/pentobi_qml/translations.qrc @@ -0,0 +1,6 @@ + + + qml/i18n/qml_de.qm + qml/i18n/replace_qtbase_de.qm + + diff --git a/src/pentobi_thumbnailer/CMakeLists.txt b/src/pentobi_thumbnailer/CMakeLists.txt new file mode 100644 index 0000000..43339b0 --- /dev/null +++ b/src/pentobi_thumbnailer/CMakeLists.txt @@ -0,0 +1,17 @@ +set(pentobi_thumbnailer_SRCS Main.cpp) + +add_executable(pentobi-thumbnailer Main.cpp) + +target_link_libraries(pentobi-thumbnailer + pentobi_thumbnail + pentobi_gui + pentobi_base + boardgame_base + boardgame_sgf + boardgame_util + boardgame_sys + ) + +target_link_libraries(pentobi-thumbnailer Qt5::Widgets) + +install(TARGETS pentobi-thumbnailer DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/pentobi_thumbnailer/Main.cpp b/src/pentobi_thumbnailer/Main.cpp new file mode 100644 index 0000000..c4f6e72 --- /dev/null +++ b/src/pentobi_thumbnailer/Main.cpp @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_thumbnailer/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#include +#include "libpentobi_thumbnail/CreateThumbnail.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +int main(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + try + { + QCommandLineParser parser; + QCommandLineOption optionSize(QStringList() << "s" << "size", + "Generate image with height and width .", + "size", "128"); + parser.addOption(optionSize); + parser.process(app); + auto args = parser.positionalArguments(); + bool ok; + int size = parser.value(optionSize).toInt(&ok); + if (! ok || size <= 0) + throw runtime_error("Invalid image size"); + if (args.size() > 2) + throw runtime_error("Too many file arguments"); + if (args.size() < 2) + throw runtime_error("Need input and output file argument"); + QImage image(size, size, QImage::Format_ARGB32); + image.fill(Qt::transparent); + if (! createThumbnail(args.at(0), size, size, image)) + throw runtime_error("Thumbnail generation failed"); + QImageWriter writer(args.at(1), "png"); + if (! writer.write(image)) + throw runtime_error(writer.errorString().toLocal8Bit().constData()); + } + catch (const exception& e) + { + cerr << e.what() << '\n'; + return 1; + } + return 0; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Analyze.cpp b/src/twogtp/Analyze.cpp new file mode 100644 index 0000000..4c1c974 --- /dev/null +++ b/src/twogtp/Analyze.cpp @@ -0,0 +1,173 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Analyze.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Analyze.h" + +#include +#include +#include +#include "libboardgame_util/FmtSaver.h" +#include "libboardgame_util/Statistics.h" +#include "libboardgame_util/StringUtil.h" + +using libboardgame_util::from_string; +using libboardgame_util::split; +using libboardgame_util::trim; +using libboardgame_util::FmtSaver; +using libboardgame_util::Statistics; +using libboardgame_util::StatisticsExt; + +//----------------------------------------------------------------------------- + +namespace { + +void write_result(const Statistics<>& stat) +{ + FmtSaver saver(cout); + cout << fixed << setprecision(1) << stat.get_mean() * 100 << "+-" + << stat.get_error() * 100; +} + +} // namespace + +//----------------------------------------------------------------------------- + +void analyze(const string& file) +{ + ifstream in(file); + Statistics<> stat_result; + map> stat_result_player; + map result_count; + StatisticsExt<> stat_length; + StatisticsExt<> stat_cpu_b; + StatisticsExt<> stat_cpu_w; + StatisticsExt<> stat_fast_open; + string line; + while (getline(in, line)) + { + line = trim(line); + if (! line.empty() && line[0] == '#') + continue; + auto columns = split(line, '\t'); + if (columns.empty()) + continue; + float result; + unsigned length; + unsigned player; + float cpu_b; + float cpu_w; + unsigned fast_open; + if (columns.size() != 7 + || ! from_string(columns[1], result) + || ! from_string(columns[2], length) + || ! from_string(columns[3], player) + || ! from_string(columns[4], cpu_b) + || ! from_string(columns[5], cpu_w) + || ! from_string(columns[6], fast_open)) + throw runtime_error("invalid format"); + stat_result.add(result); + stat_result_player[player].add(result); + ++result_count[result]; + stat_length.add(length); + stat_cpu_b.add(cpu_b); + stat_cpu_w.add(cpu_w); + stat_fast_open.add(fast_open); + } + auto count = stat_result.get_count(); + cout << "Gam: " << count; + if (count == 0) + { + cout << '\n'; + return; + } + cout << ", Res: "; + write_result(stat_result); + cout << " ("; + bool is_first = true; + for (auto& i : stat_result_player) + { + if (! is_first) + cout << ", "; + else + is_first = false; + cout << i.first << ": "; + write_result(i.second); + } + cout << ")\nResFreq:"; + for (auto& i : result_count) + { + cout << ' ' << i.first << "="; + { + FmtSaver saver(cout); + auto fraction = i.second / count; + cout << fixed << setprecision(1) << fraction * 100 + << "+-" << sqrt(fraction * (1 - fraction) / count) * 100; + } + } + cout << "\nCpuB: "; + stat_cpu_b.write(cout, true, 3, false, true); + cout << "\nCpuW: "; + stat_cpu_w.write(cout, true, 3, false, true); + auto cpu_b = stat_cpu_b.get_mean(); + auto cpu_w = stat_cpu_w.get_mean(); + auto err_cpu_b = stat_cpu_b.get_error(); + auto err_cpu_w = stat_cpu_w.get_error(); + cout << "\nCpuB/CpuW: "; + if (cpu_b > 0 && cpu_w > 0) + cout << fixed << setprecision(3) << cpu_b / cpu_w << "+-" + << cpu_b / cpu_w * hypot(err_cpu_b / cpu_b, err_cpu_w / cpu_w); + else + cout << "-"; + cout << ", Len: "; + stat_length.write(cout, true, 1, true, true); + if (stat_fast_open.get_mean() > 0) + { + cout << ", Fast: "; + stat_fast_open.write(cout, true, 1, true, true); + } + cout << '\n'; +} + +void splitsgf(const string& file) +{ + ifstream in(file); + string filename; + string buffer; + regex pattern("GN\\[(\\d+)\\]"); + string line; + while (getline(in, line)) + { + if (trim(line) == "(") + { + if (! filename.empty()) + { + ofstream out(filename); + out << buffer; + } + buffer.clear(); + } + else + { + smatch match; + regex_search(line, match, pattern); + if (! match.empty()) + filename = string(match[1]) + ".blksgf"; + } + buffer.append(line); + buffer.append("\n"); + } + if (! filename.empty()) + { + ofstream out(filename); + out << buffer; + } +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Analyze.h b/src/twogtp/Analyze.h new file mode 100644 index 0000000..e1dc754 --- /dev/null +++ b/src/twogtp/Analyze.h @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Analyze.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_ANALYZE_H +#define TWOGTP_ANALYZE_H + +#include + +using namespace std; + +//----------------------------------------------------------------------------- + +void analyze(const string& file); + +void splitsgf(const string& file); + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_ANALYZE_H diff --git a/src/twogtp/CMakeLists.txt b/src/twogtp/CMakeLists.txt new file mode 100644 index 0000000..a4dc5ec --- /dev/null +++ b/src/twogtp/CMakeLists.txt @@ -0,0 +1,28 @@ +add_executable(twogtp + Analyze.h + Analyze.cpp + FdStream.h + FdStream.cpp + GtpConnection.h + GtpConnection.cpp + Main.cpp + Output.h + Output.cpp + OutputTree.h + OutputTree.cpp + TwoGtp.h + TwoGtp.cpp +) + +target_link_libraries(twogtp + pentobi_base + boardgame_sgf + boardgame_base + boardgame_util + boardgame_sys +) + +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(twogtp ${CMAKE_THREAD_LIBS_INIT}) +endif() + diff --git a/src/twogtp/FdStream.cpp b/src/twogtp/FdStream.cpp new file mode 100644 index 0000000..5ef5c81 --- /dev/null +++ b/src/twogtp/FdStream.cpp @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/FdStream.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "FdStream.h" + +#include +#include + +//----------------------------------------------------------------------------- + +namespace { + +const size_t put_back = 1; + +} // namespace + +//----------------------------------------------------------------------------- + +FdInBuf::FdInBuf(int fd, size_t buf_size) + : m_fd(fd), + m_buf(buf_size + put_back) +{ + auto end = &(*m_buf.end()); + setg(end, end, end); +} + +FdInBuf::~FdInBuf() = default; + +auto FdInBuf::underflow() -> int_type +{ + if (gptr() < egptr()) + return traits_type::to_int_type(*gptr()); + auto base = &m_buf.front(); + auto start = base; + if (eback() == base) + { + memmove(base, egptr() - put_back, put_back); + start += put_back; + } + auto n = read(m_fd, start, m_buf.size() - (start - base)); + if (n <= 0) + return traits_type::eof(); + setg(base, start, start + n); + return traits_type::to_int_type(*gptr()); +} + +//----------------------------------------------------------------------------- + +FdInStream::FdInStream(int fd) + : istream(nullptr), + m_buf(fd) +{ + rdbuf(&m_buf); +} + +//----------------------------------------------------------------------------- + +FdOutBuf::~FdOutBuf() = default; + +auto FdOutBuf::overflow(int_type c) -> int_type +{ + if (c != traits_type::eof()) + { + char buffer[1]; + buffer[0] = static_cast(c); + if (write(m_fd, buffer, 1) != 1) + return traits_type::eof(); + } + return c; +} + +streamsize FdOutBuf::xsputn(const char_type* s, streamsize count) +{ + return write(m_fd, s, count); +} + +//----------------------------------------------------------------------------- + +FdOutStream::FdOutStream(int fd) + : ostream(nullptr), + m_buf(fd) +{ + rdbuf(&m_buf); +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/FdStream.h b/src/twogtp/FdStream.h new file mode 100644 index 0000000..25ea211 --- /dev/null +++ b/src/twogtp/FdStream.h @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/FdStream.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_FDSTREAM_H +#define TWOGTP_FDSTREAM_H + +#include +#include + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Input stream buffer from a file descriptor. */ +class FdInBuf + : public streambuf +{ +public: + FdInBuf(int fd, size_t buf_size = 1024); + + ~FdInBuf(); + +protected: + int_type underflow() override; + +private: + int m_fd; + + vector m_buf; +}; + +//----------------------------------------------------------------------------- + +/** Input stream from a file descriptor. */ +class FdInStream + : public istream +{ +public: + explicit FdInStream(int fd); + +private: + FdInBuf m_buf; +}; + +//----------------------------------------------------------------------------- + +/** Output stream buffer from a file descriptor. */ +class FdOutBuf + : public streambuf +{ +public: + explicit FdOutBuf(int fd) + : m_fd(fd) + { } + + ~FdOutBuf(); + +protected: + int_type overflow(int_type c) override; + + streamsize xsputn(const char_type* s, streamsize count) override; + +private: + int m_fd; +}; + +//----------------------------------------------------------------------------- + +/** Output stream from a file descriptor. */ +class FdOutStream + : public ostream +{ +public: + explicit FdOutStream(int fd); + +private: + FdOutBuf m_buf; +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_FDSTREAM_H diff --git a/src/twogtp/GtpConnection.cpp b/src/twogtp/GtpConnection.cpp new file mode 100644 index 0000000..2873fc6 --- /dev/null +++ b/src/twogtp/GtpConnection.cpp @@ -0,0 +1,175 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/GtpConnection.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "GtpConnection.h" + +#include +#include +#include +#include +#include "FdStream.h" +#include "libboardgame_util/Log.h" + +//----------------------------------------------------------------------------- + +namespace { + +void terminate_child(const string& message) +{ + LIBBOARDGAME_LOG(message); + exit(1); +} + +vector split_args(string s) +{ + vector result; + bool escape = false; + bool is_in_string = false; + ostringstream token; + for (auto c : s) + { + if (c == '"' && ! escape) + { + if (is_in_string) + { + result.push_back(token.str()); + token.str(""); + } + is_in_string = ! is_in_string; + } + else if (isspace(c) && ! is_in_string) + { + if (! token.str().empty()) + { + result.push_back(token.str()); + token.str(""); + } + } + else + token << c; + escape = (c == '\\' && ! escape); + } + if (! token.str().empty()) + result.push_back(token.str()); + return result; +} + +} // namespace + +//----------------------------------------------------------------------------- + +GtpConnection::GtpConnection(const string& command) +{ + vector args = split_args(command); + if (args.size() == 0) + throw runtime_error("GtpConnection: empty command line"); + int fd1[2]; + if (pipe(fd1) < 0) + throw runtime_error("GtpConnection: pipe creation failed"); + int fd2[2]; + if (pipe(fd2) < 0) + { + close(fd1[0]); + close(fd1[1]); + throw runtime_error("GtpConnection: pipe creation failed"); + } + pid_t pid; + if ((pid = fork()) < 0) + throw runtime_error("GtpConnection: fork failed"); + else if (pid > 0) // Parent + { + close(fd1[0]); + close(fd2[1]); + m_in.reset(new FdInStream(fd2[0])); + m_out.reset(new FdOutStream(fd1[1])); + return; + } + else // Child + { + close(fd1[1]); + close(fd2[0]); + if (fd1[0] != STDIN_FILENO) + if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) + { + close(fd1[0]); + terminate_child("GtpConnection: dup2 to stdin failed"); + } + if (fd2[1] != STDOUT_FILENO) + if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) + { + close(fd2[1]); + terminate_child("GtpConnection: dup2 to stdout failed"); + } + auto const argv = new char*[args.size() + 1]; + for (size_t i = 0; i < args.size(); ++i) + { + argv[i] = new char[args[i].size() + 1]; + strcpy(argv[i], args[i].c_str()); + } + argv[args.size()] = nullptr; + if (execvp(args[0].c_str(), argv) == -1) + terminate_child("Could not execute '" + command + "'"); + } +} + +GtpConnection::~GtpConnection() = default; + +void GtpConnection::enable_log(const string& prefix) +{ + m_quiet = false; + m_prefix = prefix; +} + +string GtpConnection::send(const string& command) +{ + if (! m_quiet) + LIBBOARDGAME_LOG(m_prefix, ">> ", command); + *m_out << command << '\n'; + m_out->flush(); + if (! *m_out) + throw Failure("GtpConnection: write failure"); + ostringstream response; + bool done = false; + bool is_first = true; + bool success = true; + while (! done) + { + string line; + getline(*m_in, line); + if (! *m_in) + throw Failure("GtpConnection: read failure"); + if (! m_quiet && ! line.empty()) + LIBBOARDGAME_LOG(m_prefix, "<< ", line); + if (is_first) + { + if (line.size() < 2 || (line[0] != '=' && line[0] != '?') + || line[1] != ' ') + throw Failure("GtpConnection: malformed response: '" + line + + "'"); + if (line[0] == '?') + success = false; + line = line.substr(2); + response << line; + is_first = false; + } + else + { + if (line.empty()) + done = true; + else + response << '\n' << line; + } + } + if (! success) + throw Failure(response.str()); + return response.str(); +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/GtpConnection.h b/src/twogtp/GtpConnection.h new file mode 100644 index 0000000..3c21150 --- /dev/null +++ b/src/twogtp/GtpConnection.h @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/GtpConnection.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_GTP_CONNECTION_H +#define TWOGTP_GTP_CONNECTION_H + +#include +#include +#include + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Invokes a GTP engine in an external process. */ +class GtpConnection +{ +public: + class Failure + : public runtime_error + { + using runtime_error::runtime_error; + }; + + + explicit GtpConnection(const string& command); + + ~GtpConnection(); + + void enable_log(const string& prefix = ""); + + /** Send a GTP command. + @param command The command. + @return The response if the command returns a success status. + @throws Failure If the command returns an error status. */ + string send(const string& command); + +private: + bool m_quiet = true; + + string m_prefix; + + unique_ptr m_in; + + unique_ptr m_out; +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_GTP_CONNECTION_H diff --git a/src/twogtp/Main.cpp b/src/twogtp/Main.cpp new file mode 100644 index 0000000..ea90942 --- /dev/null +++ b/src/twogtp/Main.cpp @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include "Analyze.h" +#include "TwoGtp.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/Options.h" +#include "libpentobi_base/Variant.h" + +using namespace std; +using libboardgame_util::to_string; +using libboardgame_util::Options; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +int main(int argc, char** argv) +{ + atomic result(0); + try + { + vector specs = { + "analyze:", + "black|b:", + "fastopen", + "file|f:", + "game|g:", + "nugames|n:", + "quiet", + "splitsgf:", + "threads:", + "tree", + "white|w:", + }; + Options opt(argc, argv, specs); + if (opt.contains("analyze")) + { + analyze(opt.get("analyze")); + return 0; + } + if (opt.contains("splitsgf")) + { + splitsgf(opt.get("splitsgf")); + return 0; + } + auto black = opt.get("black"); + auto white = opt.get("white"); + auto prefix = opt.get("file", "output"); + auto nu_games = opt.get("nugames", 1); + auto nu_threads = opt.get("threads", 1); + auto variant_string = opt.get("game", "classic"); + bool quiet = opt.contains("quiet"); + if (quiet) + libboardgame_util::disable_logging(); + bool fast_open = opt.contains("fastopen"); + bool create_tree = opt.contains("tree") || fast_open; + Variant variant; + if (! parse_variant_id(variant_string, variant)) + throw runtime_error("invalid game variant " + variant_string); + Output output(variant, prefix, create_tree); + vector> twogtp; + for (unsigned i = 0; i < nu_threads; ++i) + { + string log_prefix; + if (nu_threads > 1) + log_prefix = to_string(i + 1); + twogtp.push_back(make_shared(black, white, variant, + nu_games, output, quiet, + log_prefix, fast_open)); + } + vector threads; + for (auto& i : twogtp) + threads.push_back(thread([&i, &result]() + { + try + { + i->run(); + } + catch (const exception& e) + { + LIBBOARDGAME_LOG("Error: ", e.what()); + result = 1; + } + })); + for (auto& t : threads) + t.join(); + } + catch (const exception& e) + { + LIBBOARDGAME_LOG("Error: ", e.what()); + result = 1; + } + return result; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Output.cpp b/src/twogtp/Output.cpp new file mode 100644 index 0000000..e56d16a --- /dev/null +++ b/src/twogtp/Output.cpp @@ -0,0 +1,128 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Output.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Output.h" + +#include +#include +#include +#include +#include +#include +#include "libboardgame_util/StringUtil.h" + +using libboardgame_util::from_string; +using libboardgame_util::split; +using libboardgame_util::trim; + +//----------------------------------------------------------------------------- + +Output::Output(Variant variant, const string& prefix, bool create_tree) + : m_create_tree(create_tree), + m_prefix(prefix), + m_output_tree(variant) +{ + m_lock_fd = creat((prefix + ".lock").c_str(), 0644); + if (m_lock_fd == -1) + throw runtime_error("Output: could not create lock file"); + if (flock(m_lock_fd, LOCK_EX | LOCK_NB) == -1) + throw runtime_error("Output: twogtp already running"); + ifstream in(prefix + ".dat"); + if (! in) + return; + string line; + while (getline(in, line)) + { + line = trim(line); + if (! line.empty() && line[0] == '#') + continue; + auto columns = split(line, '\t'); + if (columns.empty()) + continue; + unsigned game_number; + if (! from_string(columns[0], game_number)) + throw runtime_error("Output: expected game number"); + m_games.insert(make_pair(game_number, line)); + } + while (m_games.count(m_next) != 0) + ++m_next; + if (check_sentinel()) + remove((prefix + ".stop").c_str()); + if (m_create_tree && m_next > 0) + m_output_tree.load(prefix + "-tree.blksgf"); +} + +Output::~Output() +{ + flock(m_lock_fd, LOCK_UN); + close(m_lock_fd); + remove((m_prefix + ".lock").c_str()); +} + +void Output::add_result(unsigned n, float result, const Board& bd, + unsigned player_black, double cpu_black, + double cpu_white, const string& sgf, + const array& is_real_move) +{ + lock_guard lock(m_mutex); + unsigned nu_fast_open = 0; + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + if (! is_real_move[i]) + ++nu_fast_open; + ostringstream line; + line << n << '\t' + << setprecision(4) << result << '\t' + << bd.get_nu_moves() << '\t' + << player_black << '\t' + << setprecision(5) << cpu_black << '\t' + << cpu_white << '\t' + << nu_fast_open; + m_games.insert(make_pair(n, line.str())); + { + ofstream out(m_prefix + ".dat"); + out << "# Game\tResult\tLength\tPlayerB\tCpuB\tCpuW\tFast\n"; + for (auto& i : m_games) + out << i.second << '\n'; + } + { + ofstream out(m_prefix + ".blksgf", ios::app); + out << sgf; + } + if (m_create_tree) + { + m_output_tree.add_game(bd, player_black, result, is_real_move); + m_output_tree.save(m_prefix + "-tree.blksgf"); + } +} + +bool Output::check_sentinel() +{ + return ! ifstream(m_prefix + ".stop").fail(); +} + +bool Output::generate_fast_open_move(bool is_player_black, const Board& bd, + Color to_play, Move& mv) +{ + lock_guard lock(m_mutex); + m_output_tree.generate_move(is_player_black, bd, to_play, mv); + return ! mv.is_null(); +} + +unsigned Output::get_next() +{ + lock_guard lock(m_mutex); + unsigned n = m_next; + do + ++m_next; + while (m_games.count(m_next) != 0); + return n; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Output.h b/src/twogtp/Output.h new file mode 100644 index 0000000..7dc2c7f --- /dev/null +++ b/src/twogtp/Output.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Output.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_OUTPUT_H +#define TWOGTP_OUTPUT_H + +#include +#include +#include +#include "OutputTree.h" + +//----------------------------------------------------------------------------- + +/** Handles the output files of TwoGtp and their concurrent access. */ +class Output +{ +public: + Output(Variant variant, const string& prefix, bool fastopen); + + ~Output(); + + void add_result(unsigned n, float result, const Board& bd, + unsigned player_black, double cpu_black, double cpu_white, + const string& sgf, + const array& is_real_move); + + unsigned get_next(); + + bool check_sentinel(); + + bool generate_fast_open_move(bool is_player_black, const Board& bd, + Color to_play, Move& mv); + +private: + bool m_create_tree; + + unsigned m_next = 0; + + int m_lock_fd; + + string m_prefix; + + mutex m_mutex; + + map m_games; + + OutputTree m_output_tree; +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_OUTPUT_H diff --git a/src/twogtp/OutputTree.cpp b/src/twogtp/OutputTree.cpp new file mode 100644 index 0000000..0898fd6 --- /dev/null +++ b/src/twogtp/OutputTree.cpp @@ -0,0 +1,241 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/OutputTree.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "OutputTree.h" + +#include +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_sgf/TreeWriter.h" +#include "libpentobi_base/BoardUtil.h" + +using libboardgame_sgf::SgfNode; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::TreeWriter; +using libpentobi_base::get_transforms; +using libpentobi_base::ColorMove; +using libpentobi_base::MovePoints; +using libpentobi_base::boardutil::get_transformed; + +//----------------------------------------------------------------------------- + +namespace { + +void add(PentobiTree& tree, const SgfNode& node, bool is_player_black, + bool is_real_move, float result) +{ + unsigned index = is_player_black ? 0 : 1; + array count; + array avg_result; + array real_count; + auto comment = tree.get_comment(node); + if (comment.empty()) + { + count.fill(0); + avg_result.fill(0); + real_count.fill(0); + count[index] = 1; + real_count[index] = 1; + avg_result[index] = result; + } + else + { + istringstream in(comment); + in >> count[0] >> real_count[0] >> avg_result[0] + >> count[1] >> real_count[1] >> avg_result[1]; + if (! in) + throw runtime_error("OutputTree: invalid comment: " + comment); + ++count[index]; + avg_result[index] += (result - avg_result[index]) / count[index]; + if (is_real_move) + ++real_count[index]; + } + ostringstream out; + out.precision(numeric_limits::digits10); + out << count[0] << ' ' << real_count[0] << ' ' << avg_result[0] << '\n' + << count[1] << ' ' << real_count[1] << ' ' << avg_result[1]; + tree.set_comment(node, out.str()); +} + +bool compare_sequence(ArrayList& s1, + ArrayList& s2) +{ + LIBBOARDGAME_ASSERT(s1.size() == s2.size()); + for (unsigned i = 0; i < s1.size(); ++i) + { + LIBBOARDGAME_ASSERT(s1[i].color == s2[i].color); + if (s1[i].move.to_int() < s2[i].move.to_int()) + return true; + else if (s1[i].move.to_int() > s2[i].move.to_int()) + return false; + } + return false; +} + +unsigned get_real_count(PentobiTree& tree, const SgfNode& node, + bool is_player_black) +{ + unsigned index = is_player_black ? 0 : 1; + array count; + array avg_result; + array real_count; + auto comment = tree.get_comment(node); + istringstream in(comment); + in >> count[0] >> real_count[0] >> avg_result[0] + >> count[1] >> real_count[1] >> avg_result[1]; + if (! in) + throw runtime_error("OutputTree: invalid comment: " + comment); + return real_count[index]; +} + +} // namespace + +//----------------------------------------------------------------------------- + +OutputTree::OutputTree(Variant variant) + : m_tree(variant) +{ + get_transforms(variant, m_transforms, m_inv_transforms); +} + +OutputTree::~OutputTree() +{ +} + +void OutputTree::add_game(const Board& bd, unsigned player_black, float result, + const array& is_real_move) +{ + if (bd.has_setup()) + throw runtime_error("OutputTree: setup not supported"); + + // Find the canonical representation + ArrayList sequence; + for (auto& transform : m_transforms) + { + ArrayList s; + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + auto mv = bd.get_move(i); + s.push_back(ColorMove(mv.color, + get_transformed(bd, mv.move, *transform))); + } + if (sequence.empty() || compare_sequence(s, sequence)) + sequence = s; + } + + auto node = &m_tree.get_root(); + add(m_tree, *node, player_black == 0, true, result); + unsigned nu_moves_3 = 0; + for (unsigned i = 0; i < sequence.size(); ++i) + { + unsigned player; + auto mv = sequence[i]; + Color c = mv.color; + if (bd.get_variant() == Variant::classic_3 && c == Color(3)) + { + player = nu_moves_3 % 3; + ++nu_moves_3; + } + else + player = c.to_int() % bd.get_nu_players(); + auto child = m_tree.find_child_with_move(*node, mv); + if (! child) + { + child = &m_tree.create_new_child(*node); + m_tree.set_move(*child, mv); + add(m_tree, *child, player == player_black, true, result); + return; + } + add(m_tree, *child, player == player_black, is_real_move[i], result); + node = child; + } +} + +void OutputTree::generate_move(bool is_player_black, const Board& bd, + Color to_play, Move& mv) +{ + bool play_real; + for (unsigned i = 0; i < m_transforms.size(); ++i) + { + generate_move(is_player_black, bd, to_play, *m_transforms[i], + *m_inv_transforms[i], mv, play_real); + if (play_real || ! mv.is_null()) + break; + } +} + +void OutputTree::generate_move(bool is_player_black, const Board& bd, + Color to_play, const PointTransform& transform, + const PointTransform& inv_transform, Move& mv, + bool& play_real) +{ + if (bd.has_setup()) + throw runtime_error("OutputTree: setup not supported"); + play_real = false; + mv = Move::null(); + auto node = &m_tree.get_root(); + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + auto mv = bd.get_move(i); + ColorMove transformed_mv(mv.color, + get_transformed(bd, mv.move, transform)); + auto child = m_tree.find_child_with_move(*node, transformed_mv); + if (! child) + return; + node = child; + } + unsigned sum = 0; + for (auto& i : node->get_children()) + sum += get_real_count(m_tree, i, is_player_black); + if (sum == 0) + return; + uniform_real_distribution distribution(0, 1); + if (distribution(m_random) < 1.0 / sum) + { + play_real = true; + return; + } + unsigned random = static_cast(distribution(m_random) * sum); + sum = 0; + for (auto& i : node->get_children()) + { + auto real_count = get_real_count(m_tree, i, is_player_black); + if (real_count == 0) + continue; + sum += real_count; + if (sum >= random) + { + auto color_mv = m_tree.get_move(i); + if (color_mv.is_null()) + throw runtime_error("OutputTree: tree has node without move"); + if (color_mv.color != to_play) + throw runtime_error("OutputTree: tree has node wrong move color"); + mv = get_transformed(bd, color_mv.move, inv_transform); + return; + } + } + LIBBOARDGAME_ASSERT(false); +} + +void OutputTree::load(const string& file) +{ + TreeReader reader; + reader.read(file); + auto tree = reader.get_tree_transfer_ownership(); + m_tree.init(tree); +} + +void OutputTree::save(const string& file) +{ + ofstream out(file); + TreeWriter writer(out, m_tree.get_root()); + writer.write(); +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/OutputTree.h b/src/twogtp/OutputTree.h new file mode 100644 index 0000000..9211f8d --- /dev/null +++ b/src/twogtp/OutputTree.h @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/OutputTree.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_OUTPUT_TREE_H +#define TWOGTP_OUTPUT_TREE_H + +#include +#include "libpentobi_base/Board.h" +#include "libpentobi_base/PentobiTree.h" + +using namespace std; +using libboardgame_base::PointTransform; +using libboardgame_util::ArrayList; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::Move; +using libpentobi_base::PentobiTree; +using libpentobi_base::Point; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** Merges opening moves played by the players into a tree. + + Keeps statistics of the average game result for each move and player. + This class can also speed up playing test games by generating opening moves + according to the measured probability distributions. With some probabilty, + which decreases with the number of times a position was visited but stays + non-zero, the player generates a real move, which is used to update the + distributions, otherwise a move from the tree is played. In the limit, the + player plays an infinite number of real moves in each position, so the + measured distributions approach the real distributions and the result of + the test games approaches the result as if only real moves had been + played. */ +class OutputTree +{ +public: + explicit OutputTree(Variant variant); + + ~OutputTree(); + + void load(const string& file); + + void save(const string& file); + + /** Generate a move for a player from the tree. + @param is_player_black + @param bd The board with the current position. + @param to_play The color to generate the move for.. + @param[out] mv The generated move, or Move::null() if no move is in the + tree for this position or if the player should generate a real move + now. */ + void generate_move(bool is_player_black, const Board& bd, Color to_play, + Move& mv); + + /** Add the moves of a game to the tree and update the move counters. */ + void add_game(const Board& bd, unsigned player_black, float result, + const array& is_real_move); + +private: + typedef libboardgame_base::PointTransform PointTransform; + + PentobiTree m_tree; + + vector> m_transforms; + + vector> m_inv_transforms; + + mt19937 m_random; + + void generate_move(bool is_player_black, const Board& bd, Color to_play, + const PointTransform& transform, + const PointTransform& inv_transform, Move& mv, + bool& play_real); +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_OUTPUT_TREE_H diff --git a/src/twogtp/TwoGtp.cpp b/src/twogtp/TwoGtp.cpp new file mode 100644 index 0000000..ced86db --- /dev/null +++ b/src/twogtp/TwoGtp.cpp @@ -0,0 +1,195 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/TwoGtp.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TwoGtp.h" + +#include "libboardgame_sgf/Writer.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/StringUtil.h" +#include "libpentobi_base/ScoreUtil.h" + +using libboardgame_sgf::Writer; +using libboardgame_util::trim; +using libpentobi_base::get_multiplayer_result; +using libpentobi_base::Move; +using libpentobi_base::PieceSet; +using libpentobi_base::ScoreType; + +//----------------------------------------------------------------------------- + +TwoGtp::TwoGtp(const string& black, const string& white, Variant variant, + unsigned nu_games, Output& output, bool quiet, + const string& log_prefix, bool fast_open) + : m_quiet(quiet), + m_fast_open(fast_open), + m_variant(variant), + m_nu_games(nu_games), + m_bd(variant), + m_output(output), + m_black(black), + m_white(white) +{ + if (! m_quiet) + { + m_black.enable_log(log_prefix + "B"); + m_white.enable_log(log_prefix + "W"); + } + if (get_nu_colors(m_variant) == 2) + { + m_colors[0] = "b"; + m_colors[1] = "w"; + } + else + { + m_colors[0] = "1"; + m_colors[1] = "2"; + m_colors[2] = "3"; + m_colors[3] = "4"; + } +} + +float TwoGtp::get_result(unsigned player_black) +{ + float result; + auto nu_players = m_bd.get_nu_players(); + if (nu_players == 2) + { + auto score = m_bd.get_score_twoplayer(Color(0)); + if (score > 0) + result = 1; + else if (score < 0) + result = 0; + else + result = 0.5; + if (player_black != 0) + result = 1 - result; + } + else + { + array points; + for (Color::IntType i = 0; i < m_bd.get_nu_colors(); ++i) + points[i] = m_bd.get_points(Color(i)); + array player_result; + bool break_ties = (m_bd.get_piece_set() == PieceSet::callisto); + get_multiplayer_result(nu_players, points, player_result, break_ties); + result = player_result[player_black]; + } + return result; +} + +void TwoGtp::play_game(unsigned game_number) +{ + if (! m_quiet) + LIBBOARDGAME_LOG("================================================\n" + "Game ", game_number, "\n" + "================================================"); + m_bd.init(); + send_both("clear_board"); + auto cpu_black = send_cputime(m_black); + auto cpu_white = send_cputime(m_white); + unsigned nu_players = m_bd.get_nu_players(); + unsigned player_black = game_number % nu_players; + bool resign = false; + ostringstream sgf_string; + Writer sgf(sgf_string); + sgf.set_indent(0); + sgf.begin_tree(); + sgf.begin_node(); + sgf.write_property("GM", to_string(m_variant)); + sgf.write_property("GN", game_number); + sgf.end_node(); + array is_real_move; + unsigned player; + while (! m_bd.is_game_over()) + { + auto to_play = m_bd.get_effective_to_play(); + if (m_variant == Variant::classic_3 && to_play == Color(3)) + player = m_bd.get_alt_player(); + else + player = to_play.to_int() % nu_players; + auto& player_connection = (player == player_black ? m_black : m_white); + auto& other_connection = (player == player_black ? m_white : m_black); + auto color = m_colors[to_play.to_int()]; + Move mv; + if (m_fast_open + && m_output.generate_fast_open_move(player == player_black, + m_bd, to_play, mv)) + { + is_real_move[m_bd.get_nu_moves()] = false; + LIBBOARDGAME_LOG("Playing fast opening move"); + player_connection.send("play " + color + " " + m_bd.to_string(mv)); + } + else + { + is_real_move[m_bd.get_nu_moves()] = true; + auto response = player_connection.send("genmove " + color); + if (response == "resign") + { + resign = true; + break; + } + mv = m_bd.from_string(response); + } + sgf.begin_node(); + sgf.write_property(string(1, static_cast(toupper(color[0]))), + m_bd.to_string(mv)); + sgf.end_node(); + if (mv.is_null() || ! m_bd.is_legal(to_play, mv)) + throw runtime_error("invalid move: " + m_bd.to_string(mv)); + m_bd.play(to_play, mv); + other_connection.send("play " + color + " " + m_bd.to_string(mv)); + } + cpu_black = send_cputime(m_black) - cpu_black; + cpu_white = send_cputime(m_white) - cpu_white; + float result; + if (resign) + { + if (nu_players > 2) + throw runtime_error("resign only allowed in two-player variants"); + result = (player == player_black ? 0 : 1); + } + else + result = get_result(player_black); + sgf.end_tree(); + m_output.add_result(game_number, result, m_bd, player_black, cpu_black, + cpu_white, sgf_string.str(), is_real_move); +} + +void TwoGtp::run() +{ + send_both(string("set_game ") + to_string(m_variant)); + while (! m_output.check_sentinel()) + { + unsigned n = m_output.get_next(); + if (n >= m_nu_games) + break; + play_game(n); + } + send_both("quit"); +} + +void TwoGtp::send_both(const string& cmd) +{ + m_black.send(cmd); + m_white.send(cmd); +} + +double TwoGtp::send_cputime(GtpConnection& gtp_connection) +{ + string response = gtp_connection.send("cputime"); + istringstream in(response); + double cputime; + in >> cputime; + if (! in) + throw runtime_error("invalid response to cputime: " + response); + return cputime; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/TwoGtp.h b/src/twogtp/TwoGtp.h new file mode 100644 index 0000000..59116f9 --- /dev/null +++ b/src/twogtp/TwoGtp.h @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/TwoGtp.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_TWOGTP_H +#define TWOGTP_TWOGTP_H + +#include +#include "GtpConnection.h" +#include "Output.h" +#include "libpentobi_base/Board.h" + +using namespace std; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class TwoGtp +{ +public: + TwoGtp(const string& black, const string& white, Variant variant, + unsigned nu_games, Output& output, bool quiet, + const string& log_prefix, bool fast_open); + + void run(); + +private: + bool m_quiet; + + bool m_fast_open; + + Variant m_variant; + + unsigned m_nu_games; + + Board m_bd; + + Output& m_output; + + GtpConnection m_black; + + GtpConnection m_white; + + array m_colors; + + float get_result(unsigned player_black); + + void play_game(unsigned game_number); + + void send_both(const string& cmd); + + double send_cputime(GtpConnection& gtp_connection); +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_TWOGTP_H diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt new file mode 100644 index 0000000..88fb251 --- /dev/null +++ b/src/unittest/CMakeLists.txt @@ -0,0 +1,10 @@ +add_subdirectory(libboardgame_util) +add_subdirectory(libboardgame_sgf) +add_subdirectory(libboardgame_base) +add_subdirectory(libboardgame_mcts) +add_subdirectory(libpentobi_base) +add_subdirectory(libpentobi_mcts) + +if (PENTOBI_BUILD_GTP) + add_subdirectory(libboardgame_gtp) +endif() diff --git a/src/unittest/libboardgame_base/CMakeLists.txt b/src/unittest/libboardgame_base/CMakeLists.txt new file mode 100644 index 0000000..8ef1b1d --- /dev/null +++ b/src/unittest/libboardgame_base/CMakeLists.txt @@ -0,0 +1,17 @@ +add_executable(unittest_libboardgame_base + MarkerTest.cpp + PointTransformTest.cpp + RatingTest.cpp + RectGeometryTest.cpp + StringRepTest.cpp +) + +target_link_libraries(unittest_libboardgame_base + boardgame_test_main + boardgame_base + boardgame_test + boardgame_util + boardgame_sys + ) + +add_test(libboardgame_base unittest_libboardgame_base) diff --git a/src/unittest/libboardgame_base/MarkerTest.cpp b/src/unittest/libboardgame_base/MarkerTest.cpp new file mode 100644 index 0000000..255d89a --- /dev/null +++ b/src/unittest/libboardgame_base/MarkerTest.cpp @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_base/MarkerTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_base/Marker.h" +#include "libboardgame_base/Point.h" +#include "libboardgame_test/Test.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +typedef libboardgame_base::Point<19 * 19, 19, 19, unsigned short> Point; +typedef libboardgame_base::Marker Marker; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(boardgame_marker_basic) +{ + Marker m; + Point p1(10); + Point p2(11); + LIBBOARDGAME_CHECK(! m.set(p1)); + LIBBOARDGAME_CHECK(! m.set(p2)); + LIBBOARDGAME_CHECK(m.set(p1)); + LIBBOARDGAME_CHECK(m.set(p2)); + m.clear(); + LIBBOARDGAME_CHECK(! m.set(p1)); + LIBBOARDGAME_CHECK(! m.set(p2)); +} + +/** Test clear after a number of clears around the maximum unsigned integer + value. + This is a critical point of the implementation, which assumes that + values not equal to a clear counter are unmarked and the overflow of the + clear counter must be handled correctly. + This test is only run, if integers are not larger than 32-bit, otherwise + it would take too long. */ +LIBBOARDGAME_TEST_CASE(boardgame_marker_overflow) +{ + if (numeric_limits::digits > 32) + return; + Marker m; + m.setup_for_overflow_test(numeric_limits::max() - 5); + Point p1(10); + Point p2(11); + for (int i = 0; i < 10; ++i) + { + LIBBOARDGAME_CHECK(! m.set(p1)); + LIBBOARDGAME_CHECK(! m.set(p2)); + m.clear(); + } +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/PointTransformTest.cpp b/src/unittest/libboardgame_base/PointTransformTest.cpp new file mode 100644 index 0000000..2731b1e --- /dev/null +++ b/src/unittest/libboardgame_base/PointTransformTest.cpp @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_base/PointTransformTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_base/Point.h" +#include "libboardgame_base/PointTransform.h" +#include "libboardgame_base/RectGeometry.h" +#include "libboardgame_test/Test.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +typedef libboardgame_base::Point<19 * 19, 19, 19, unsigned short> Point; +typedef libboardgame_base::RectGeometry RectGeometry; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(boardgame_point_transform_get_transformed) +{ + unsigned sz = 9; + auto& geo = RectGeometry::get(sz, sz); + Point p = geo.get_point(1, 2); + { + libboardgame_base::PointTransfIdent transform; + LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) == p); + } + { + libboardgame_base::PointTransfRot180 transform; + LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) + == geo.get_point(7, 6)); + } + { + libboardgame_base::PointTransfRot270Refl transform; + LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) + == geo.get_point(2, 1)); + } +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/RatingTest.cpp b/src/unittest/libboardgame_base/RatingTest.cpp new file mode 100644 index 0000000..2ec5031 --- /dev/null +++ b/src/unittest/libboardgame_base/RatingTest.cpp @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_base/RatingTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_base/Rating.h" +#include "libboardgame_test/Test.h" + +using namespace libboardgame_base; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(boardgame_rating_get_expected_result) +{ + Rating a(2806); + Rating b(2577); + LIBBOARDGAME_CHECK_CLOSE_EPS(a.get_expected_result(b), 0.789, 0.001); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rating_get_expected_result_multiplayer) +{ + // Player and 3 opponents, all with rating 1000, should have 25% + // winning probability + Rating a(1000); + Rating b(1000); + LIBBOARDGAME_CHECK_CLOSE_EPS(a.get_expected_result(b, 3), 0.25, 0.001); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rating_update_1) +{ + Rating a(2806); + Rating b(2577); + Rating new_a = a; + Rating new_b = b; + new_a.update(0, b, 10); + new_b.update(1, a, 10); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_a.get(), 2798.f, 1); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2585.f, 1); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rating_update_2) +{ + Rating a(2806); + Rating b(2577); + Rating new_a = a; + Rating new_b = b; + new_a.update(1, b, 10); + new_b.update(0, a, 10); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_a.get(), 2808.f, 1); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2575.f, 1); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rating_update_3) +{ + Rating a(2806); + Rating b(2577); + Rating new_a = a; + Rating new_b = b; + new_a.update(0.5, b, 10); + new_b.update(0.5, a, 10); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_a.get(), 2803.f, 1); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2580.f, 1); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/RectGeometryTest.cpp b/src/unittest/libboardgame_base/RectGeometryTest.cpp new file mode 100644 index 0000000..56a4a73 --- /dev/null +++ b/src/unittest/libboardgame_base/RectGeometryTest.cpp @@ -0,0 +1,90 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_base/RectGeometryTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_base/Point.h" +#include "libboardgame_base/RectGeometry.h" +#include "libboardgame_test/Test.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +typedef libboardgame_base::Point<19 * 19, 19, 19, unsigned short> Point; +typedef libboardgame_base::Geometry Geometry; +typedef libboardgame_base::RectGeometry RectGeometry; +typedef libboardgame_base::ArrayList PointList; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(boardgame_rect_geometry_iterate) +{ + auto& geo = RectGeometry::get(3, 3); + auto i = geo.begin(); + auto end = geo.end(); + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(0, 0) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(1, 0) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(2, 0) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(0, 1) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(1, 1) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(2, 1) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(0, 2) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(1, 2) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(2, 2) == *i); + ++i; + LIBBOARDGAME_CHECK(i == end); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rect_geometry_from_string) +{ + auto& geo = RectGeometry::get(19, 19); + Point p; + + LIBBOARDGAME_CHECK(geo.from_string("a1", p)); + LIBBOARDGAME_CHECK(p == geo.get_point(0, 18)); + + LIBBOARDGAME_CHECK(geo.from_string("a19", p)); + LIBBOARDGAME_CHECK(p == geo.get_point(0, 0)); + + LIBBOARDGAME_CHECK(geo.from_string("A1", p)); + LIBBOARDGAME_CHECK(p == geo.get_point(0, 18)); + + LIBBOARDGAME_CHECK(! geo.from_string("foobar", p)); + LIBBOARDGAME_CHECK(! geo.from_string("a123", p)); + LIBBOARDGAME_CHECK(! geo.from_string("a56", p)); + LIBBOARDGAME_CHECK(! geo.from_string("aa1", p)); + LIBBOARDGAME_CHECK(! geo.from_string("c3#", p)); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rect_geometry_to_string) +{ + auto& geo = RectGeometry::get(19, 19); + LIBBOARDGAME_CHECK_EQUAL(string("a1"), geo.to_string(geo.get_point(0, 18))); + LIBBOARDGAME_CHECK_EQUAL(string("a19"), geo.to_string(geo.get_point(0, 0))); + LIBBOARDGAME_CHECK_EQUAL(string("j10"), geo.to_string(geo.get_point(9, 9))); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/StringRepTest.cpp b/src/unittest/libboardgame_base/StringRepTest.cpp new file mode 100644 index 0000000..e348653 --- /dev/null +++ b/src/unittest/libboardgame_base/StringRepTest.cpp @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_base/StringRepTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "libboardgame_base/StringRep.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using libboardgame_base::StdStringRep; + +//----------------------------------------------------------------------------- + +namespace { + +StdStringRep string_rep; + +bool read(const string& s, unsigned& x, unsigned& y, unsigned width, + unsigned height) +{ + istringstream in(s); + return string_rep.read(in, width, height, x, y); +} + +string write(unsigned x, unsigned y, unsigned width, unsigned height) +{ + ostringstream out; + string_rep.write(out, x, y, width, height); + return out.str(); +} + +} // namespace + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(boardgame_base_spreadsheet_string_rep_read) +{ + unsigned x; + unsigned y; + + LIBBOARDGAME_CHECK(read("a1", x, y, 20, 20)); + LIBBOARDGAME_CHECK_EQUAL(x, 0u); + LIBBOARDGAME_CHECK_EQUAL(y, 19u); + + LIBBOARDGAME_CHECK(read("a23", x, y, 25, 25)); + LIBBOARDGAME_CHECK_EQUAL(x, 0u); + LIBBOARDGAME_CHECK_EQUAL(y, 2u); + + LIBBOARDGAME_CHECK(read("A1", x, y, 20, 20)); + LIBBOARDGAME_CHECK_EQUAL(x, 0u); + LIBBOARDGAME_CHECK_EQUAL(y, 19u); + + LIBBOARDGAME_CHECK(read("j1", x, y, 20, 20)); + LIBBOARDGAME_CHECK_EQUAL(x, 9u); + LIBBOARDGAME_CHECK_EQUAL(y, 19u); + + LIBBOARDGAME_CHECK(read("ab1", x, y, 30, 30)); + LIBBOARDGAME_CHECK_EQUAL(x, 27u); + LIBBOARDGAME_CHECK_EQUAL(y, 29u); + + LIBBOARDGAME_CHECK(read(" a1", x, y, 20, 20)); + LIBBOARDGAME_CHECK_EQUAL(x, 0u); + LIBBOARDGAME_CHECK_EQUAL(y, 19u); + + LIBBOARDGAME_CHECK(! read("a 1", x, y, 20, 20)); + + LIBBOARDGAME_CHECK(! read("foobar", x, y, 20, 20)); + + LIBBOARDGAME_CHECK(! read("c3#", x, y, 20, 20)); +} + +LIBBOARDGAME_TEST_CASE(boardgame_base_spreadsheet_string_rep_write) +{ + LIBBOARDGAME_CHECK_EQUAL(string("a1"), write(0, 18, 19, 19)); + LIBBOARDGAME_CHECK_EQUAL(string("a19"), write(0, 0, 19, 19)); + LIBBOARDGAME_CHECK_EQUAL(string("ab1"), write(27, 59, 60, 60)); + LIBBOARDGAME_CHECK_EQUAL(string("ba1"), write(52, 59, 60, 60)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/ArgumentsTest.cpp b/src/unittest/libboardgame_gtp/ArgumentsTest.cpp new file mode 100644 index 0000000..a42cae9 --- /dev/null +++ b/src/unittest/libboardgame_gtp/ArgumentsTest.cpp @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_gtp/ArgumentsTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_gtp/Arguments.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_gtp; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(gtp_arguments_arg) +{ + CmdLine line("command arg1 \"arg2 \" arg3 "); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL("arg1", string(args.get(0))); + LIBBOARDGAME_CHECK_EQUAL("arg2 ", string(args.get(1))); + LIBBOARDGAME_CHECK_EQUAL("arg3", string(args.get(2))); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_to_lower) +{ + CmdLine line("command cAsE"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(string("case"), args.get_tolower(0)); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_bool) +{ + { + CmdLine line("command 0"); + Arguments args(line); + LIBBOARDGAME_CHECK(! args.parse(0)); + } + { + CmdLine line("command 1"); + Arguments args(line); + LIBBOARDGAME_CHECK(args.parse(0)); + } + { + CmdLine line("command 2"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + } + { + CmdLine line("command arg1"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + } + { + CmdLine line("command"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + } +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_float) +{ + CmdLine line("command abc 5.5"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + LIBBOARDGAME_CHECK_CLOSE(5.5f, args.parse(1), 1e-4); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_int) +{ + CmdLine line("command 5 arg"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse(0)); + LIBBOARDGAME_CHECK_THROW(args.parse(1), Failure); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_min_int) +{ + CmdLine line("command 5"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse_min(0, 3)); + LIBBOARDGAME_CHECK_THROW(args.parse_min(0, 7), Failure); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_min_max_int) +{ + CmdLine line("command 5"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse_min_max(0, 3, 10)); + LIBBOARDGAME_CHECK_THROW(args.parse_min_max(0, 0, 4), Failure); + LIBBOARDGAME_CHECK_THROW(args.parse_min_max(0, 10, 20), Failure); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_single_int) +{ + { + CmdLine line("command 5"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse()); + } + { + CmdLine line("command 5 10"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(), Failure); + } +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_nu_arg_0) +{ + CmdLine line("1 command"); + Arguments args(line); + LIBBOARDGAME_CHECK_NO_THROW(args.check_empty()); + LIBBOARDGAME_CHECK_THROW(args.check_size(1), Failure); + LIBBOARDGAME_CHECK_NO_THROW(args.check_size_less_equal(2)); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_nu_arg_3) +{ + CmdLine line("command arg1 arg2 arg3"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.check_empty(), Failure); + LIBBOARDGAME_CHECK_THROW(args.check_size(2), Failure); + LIBBOARDGAME_CHECK_NO_THROW(args.check_size(3)); + LIBBOARDGAME_CHECK_THROW(args.check_size(4), Failure); + LIBBOARDGAME_CHECK_THROW(args.check_size_less_equal(2), Failure); + LIBBOARDGAME_CHECK_NO_THROW(args.check_size_less_equal(3)); + LIBBOARDGAME_CHECK_NO_THROW(args.check_size_less_equal(4)); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_remaining_arg) +{ + CmdLine line("command arg1 arg2"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL("arg2", string(args.get_remaining_line(0))); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_remaining_arg_empty) +{ + CmdLine line("command arg1"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL("", string(args.get_remaining_line(0))); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_remaining_line) +{ + CmdLine line("command arg1 \"arg2 \" arg3 "); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL("\"arg2 \" arg3", + string(args.get_remaining_line(0))); + LIBBOARDGAME_CHECK_EQUAL("arg3", string(args.get_remaining_line(1))); + LIBBOARDGAME_CHECK_EQUAL("", string(args.get_remaining_line(2))); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/CMakeLists.txt b/src/unittest/libboardgame_gtp/CMakeLists.txt new file mode 100644 index 0000000..ecd1ca1 --- /dev/null +++ b/src/unittest/libboardgame_gtp/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(unittest_libboardgame_gtp + ArgumentsTest.cpp + CmdLineTest.cpp + EngineTest.cpp + ResponseTest.cpp +) + +target_link_libraries(unittest_libboardgame_gtp + boardgame_test_main + boardgame_test + boardgame_util + boardgame_gtp + ) + +add_test(libboardgame_gtp unittest_libboardgame_gtp) diff --git a/src/unittest/libboardgame_gtp/CmdLineTest.cpp b/src/unittest/libboardgame_gtp/CmdLineTest.cpp new file mode 100644 index 0000000..8ef988d --- /dev/null +++ b/src/unittest/libboardgame_gtp/CmdLineTest.cpp @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_gtp/CmdLineTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_gtp/CmdLine.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_gtp; + +//----------------------------------------------------------------------------- + +namespace { + +string get_id(const CmdLine& c) +{ + ostringstream s; + c.write_id(s); + return s.str(); +} + +string get_element(const CmdLine& c, unsigned i) +{ + return string(c.get_element(i)); +} + +} + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(gtp_cmd_line_init) +{ + CmdLine c("100 command1 arg1 arg2"); + LIBBOARDGAME_CHECK_EQUAL("100", get_id(c)); + LIBBOARDGAME_CHECK_EQUAL("command1", string(c.get_name())); + LIBBOARDGAME_CHECK_EQUAL(4u, c.get_elements().size()); + LIBBOARDGAME_CHECK_EQUAL("arg1", get_element(c, 2)); + LIBBOARDGAME_CHECK_EQUAL("arg2", get_element(c, 3)); + c.init("2 command2 arg3"); + LIBBOARDGAME_CHECK_EQUAL("2", get_id(c)); + LIBBOARDGAME_CHECK_EQUAL("command2", string(c.get_name())); + LIBBOARDGAME_CHECK_EQUAL(3u, c.get_elements().size()); + LIBBOARDGAME_CHECK_EQUAL("arg3", get_element(c, 2)); +} + +LIBBOARDGAME_TEST_CASE(gtp_cmd_line_parse) +{ + CmdLine c("10 boardsize 11"); + LIBBOARDGAME_CHECK_EQUAL("10 boardsize 11", c.get_line()); + LIBBOARDGAME_CHECK_EQUAL("11", string(c.get_trimmed_line_after_elem(1))); + LIBBOARDGAME_CHECK_EQUAL("10", get_id(c)); + LIBBOARDGAME_CHECK_EQUAL("boardsize", string(c.get_name())); + LIBBOARDGAME_CHECK_EQUAL(3u, c.get_elements().size()); + LIBBOARDGAME_CHECK_EQUAL("11", get_element(c, 2)); + + c.init(" 20 clear_board "); + LIBBOARDGAME_CHECK_EQUAL(" 20 clear_board ", c.get_line()); + LIBBOARDGAME_CHECK_EQUAL("", string(c.get_trimmed_line_after_elem(1))); + LIBBOARDGAME_CHECK_EQUAL("20", get_id(c)); + LIBBOARDGAME_CHECK_EQUAL("clear_board", string(c.get_name())); + LIBBOARDGAME_CHECK_EQUAL(2u, c.get_elements().size()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/EngineTest.cpp b/src/unittest/libboardgame_gtp/EngineTest.cpp new file mode 100644 index 0000000..a501e0c --- /dev/null +++ b/src/unittest/libboardgame_gtp/EngineTest.cpp @@ -0,0 +1,121 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_gtp/EngineTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_gtp/Engine.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_gtp; + +//----------------------------------------------------------------------------- + +namespace { + +//----------------------------------------------------------------------------- + +/** GTP engine returning invalid responses for testing class Engine. + For testing that the base class Engine sanitizes responses of + subclasses that contain empty lines (see @ref Engine::exec_main_loop). */ +class InvalidResponseEngine + : public Engine +{ +public: + InvalidResponseEngine(); + + void invalid_response(const Arguments&, Response&); + + void invalid_response_2(const Arguments&, Response&); +}; + +InvalidResponseEngine::InvalidResponseEngine() +{ + add("invalid_response", &InvalidResponseEngine::invalid_response); + add("invalid_response_2", &InvalidResponseEngine::invalid_response_2); +} + +void InvalidResponseEngine::invalid_response(const Arguments&, Response& r) +{ + r << "This response is invalid\n" + << "\n" + << "because it contains an empty line"; +} + +void InvalidResponseEngine::invalid_response_2(const Arguments&, Response& r) +{ + r << "This response is invalid\n" + << "\n" + << "\n" + << "because it contains two empty lines"; +} + +//----------------------------------------------------------------------------- + +} // namespace + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(gtp_engine_command) +{ + istringstream in("version\n"); + ostringstream out; + Engine engine; + engine.exec_main_loop(in, out); + LIBBOARDGAME_CHECK_EQUAL(string("= \n\n"), out.str()); +} + +LIBBOARDGAME_TEST_CASE(gtp_engine_command_with_id) +{ + istringstream in("10 version\n"); + ostringstream out; + Engine engine; + engine.exec_main_loop(in, out); + LIBBOARDGAME_CHECK_EQUAL(string("=10 \n\n"), out.str()); +} + +/** Check that invalid responses with one empty line are sanitized. */ +LIBBOARDGAME_TEST_CASE(gtp_engine_empty_lines) +{ + istringstream in("invalid_response\n"); + ostringstream out; + InvalidResponseEngine engine; + engine.exec_main_loop(in, out); + LIBBOARDGAME_CHECK_EQUAL(string("= This response is invalid\n" + " \n" + "because it contains an empty line\n" + "\n"), + out.str()); +} + +/** Check that invalid responses with two empty lines are sanitized. */ +LIBBOARDGAME_TEST_CASE(gtp_engine_empty_lines_2) +{ + istringstream in("invalid_response_2\n"); + ostringstream out; + InvalidResponseEngine engine; + engine.exec_main_loop(in, out); + LIBBOARDGAME_CHECK_EQUAL(string("= This response is invalid\n" + " \n" + " \n" + "because it contains two empty lines\n" + "\n"), + out.str()); +} + +LIBBOARDGAME_TEST_CASE(gtp_engine_unknown_command) +{ + istringstream in("unknowncommand\n"); + ostringstream out; + Engine engine; + engine.exec_main_loop(in, out); + LIBBOARDGAME_CHECK(out.str().size() >= 2); + LIBBOARDGAME_CHECK_EQUAL(string("? "), out.str().substr(0, 2)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/ResponseTest.cpp b/src/unittest/libboardgame_gtp/ResponseTest.cpp new file mode 100644 index 0000000..d025021 --- /dev/null +++ b/src/unittest/libboardgame_gtp/ResponseTest.cpp @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_gtp/ResponseTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_gtp/Response.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_gtp; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(gtp_response_basic) +{ + Response r; + r << "Name"; + LIBBOARDGAME_CHECK_EQUAL(string("Name"), r.to_string()); + r.set("Name2"); + LIBBOARDGAME_CHECK_EQUAL(string("Name2"), r.to_string()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_mcts/CMakeLists.txt b/src/unittest/libboardgame_mcts/CMakeLists.txt new file mode 100644 index 0000000..83ca0a6 --- /dev/null +++ b/src/unittest/libboardgame_mcts/CMakeLists.txt @@ -0,0 +1,13 @@ +add_executable(unittest_libboardgame_mcts + NodeTest.cpp +) + +target_link_libraries(unittest_libboardgame_mcts + boardgame_test_main + boardgame_test + boardgame_sgf + boardgame_util + boardgame_sys + ) + +add_test(libboardgame_mcts unittest_libboardgame_mcts) diff --git a/src/unittest/libboardgame_mcts/NodeTest.cpp b/src/unittest/libboardgame_mcts/NodeTest.cpp new file mode 100644 index 0000000..e09bffe --- /dev/null +++ b/src/unittest/libboardgame_mcts/NodeTest.cpp @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_mcts/NodeTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_mcts/Node.h" + +#include "libboardgame_test/Test.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(libboardgame_mcts_node_add_value) +{ + libboardgame_mcts::Node node; + node.init(0, 0.5, 0); + node.add_value(5); + LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 5., 1e-4); + node.add_value(2); + LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 3.5, 1e-4); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_mcts_node_add_value_remove_loss) +{ + libboardgame_mcts::Node node; + node.init(0, 0.5, 0); + node.add_value(5); + LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 5., 1e-4); + node.add_value(0); + LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 2.5, 1e-4); + node.add_value_remove_loss(2); + LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 3.5, 1e-4); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_sgf/CMakeLists.txt b/src/unittest/libboardgame_sgf/CMakeLists.txt new file mode 100644 index 0000000..b50e7b8 --- /dev/null +++ b/src/unittest/libboardgame_sgf/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable(unittest_libboardgame_sgf + SgfNodeTest.cpp + SgfUtilTest.cpp + TreeReaderTest.cpp +) + +target_link_libraries(unittest_libboardgame_sgf + boardgame_test_main + boardgame_test + boardgame_sgf + boardgame_util + ) + +add_test(libboardgame_sgf unittest_libboardgame_sgf) diff --git a/src/unittest/libboardgame_sgf/SgfNodeTest.cpp b/src/unittest/libboardgame_sgf/SgfNodeTest.cpp new file mode 100644 index 0000000..7acd4ca --- /dev/null +++ b/src/unittest/libboardgame_sgf/SgfNodeTest.cpp @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_sgf/SgfNodeTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "libboardgame_sgf/SgfNode.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_sgf; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(sgf_node_create_new_child) +{ + unique_ptr parent(new SgfNode); + auto& child = parent->create_new_child(); + LIBBOARDGAME_CHECK_EQUAL(&parent->get_child(), &child); + LIBBOARDGAME_CHECK_EQUAL(&child.get_parent(), parent.get()); +} + +LIBBOARDGAME_TEST_CASE(sgf_node_remove_property) +{ + string id = "B"; + unique_ptr node(new SgfNode); + LIBBOARDGAME_CHECK(! node->has_property(id)); + node->set_property(id, "foo"); + LIBBOARDGAME_CHECK(node->has_property(id)); + LIBBOARDGAME_CHECK_EQUAL(node->get_property(id), "foo"); + bool result = node->remove_property(id); + LIBBOARDGAME_CHECK(result); + LIBBOARDGAME_CHECK(! node->has_property(id)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_sgf/SgfUtilTest.cpp b/src/unittest/libboardgame_sgf/SgfUtilTest.cpp new file mode 100644 index 0000000..6421fb8 --- /dev/null +++ b/src/unittest/libboardgame_sgf/SgfUtilTest.cpp @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_sgf/SgfUtilTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_sgf/SgfUtil.h" + +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_sgf; +using namespace libboardgame_sgf::util; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(sgf_util_get_path_from_root) +{ + unique_ptr root(new SgfNode); + auto& child = root->create_new_child(); + vector path; + get_path_from_root(child, path); + LIBBOARDGAME_CHECK_EQUAL(path.size(), 2u); + LIBBOARDGAME_CHECK_EQUAL(path[0], root.get()); + LIBBOARDGAME_CHECK_EQUAL(path[1], &child); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_sgf/TreeReaderTest.cpp b/src/unittest/libboardgame_sgf/TreeReaderTest.cpp new file mode 100644 index 0000000..6db65c1 --- /dev/null +++ b/src/unittest/libboardgame_sgf/TreeReaderTest.cpp @@ -0,0 +1,122 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_sgf/TreeReaderTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_sgf/TreeReader.h" + +#include +#include "libboardgame_sgf/TreeWriter.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_sgf; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_basic) +{ + istringstream in("(;B[aa];W[bb])"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK(root.has_property("B")); + LIBBOARDGAME_CHECK(root.has_single_child()); + auto& child = root.get_child(); + LIBBOARDGAME_CHECK(child.has_property("W")); + LIBBOARDGAME_CHECK(! child.has_children()); +} + +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_basic_2) +{ + istringstream in("(;C[1](;C[2.1])(;C[2.2]))"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1"); + LIBBOARDGAME_CHECK_EQUAL(root.get_nu_children(), 2u); + LIBBOARDGAME_CHECK_EQUAL(root.get_child(0).get_property("C"), "2.1"); + LIBBOARDGAME_CHECK_EQUAL(root.get_child(1).get_property("C"), "2.2"); +} + +/** Test that a property value with a unicode character is preserved after + reading and writing. + In previous versions this was broken because of a bug in the replacement + of non-newline whitespaces (as required by SGF) by the writer. (The bug + occurred only on some platforms depending on the std::isspace() + implementation.) */ +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_unicode) +{ + SgfNode root; + const char* id = "C"; + const char* value = "\xc3\xbc"; // German u-umlaut as UTF-8 + root.set_property(id, value); + ostringstream out; + TreeWriter writer(out, root); + writer.write(); + istringstream in(out.str()); + TreeReader reader; + reader.read(in); + LIBBOARDGAME_CHECK_EQUAL(reader.get_tree().get_property(id), value); +} + +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_property_after_newline) +{ + istringstream in("(;FF[4]\n" + "CA[UTF-8])"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK(root.has_property("FF")); + LIBBOARDGAME_CHECK(root.has_property("CA")); +} + +/** Test cross-platform handling of property values containing newlines. + The reader should convert all platform-dependent newline sequences (LF, + CR+LF, CR) into LF, such that property values containing newlines are + independent on the platform that was used to write the file. */ +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_newline) +{ + { + istringstream in("(;C[1\n2])"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1\n2"); + } + { + istringstream in("(;C[1\r\n2])"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1\n2"); + } + { + istringstream in("(;C[1\r2])"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1\n2"); + } +} + +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_property_without_value) +{ + istringstream in("(;B)"); + TreeReader reader; + LIBBOARDGAME_CHECK_THROW(reader.read(in), TreeReader::ReadError); +} + +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_text_before_node) +{ + istringstream in("(B;)"); + TreeReader reader; + LIBBOARDGAME_CHECK_THROW(reader.read(in), TreeReader::ReadError); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/ArrayListTest.cpp b/src/unittest/libboardgame_util/ArrayListTest.cpp new file mode 100644 index 0000000..05f00c1 --- /dev/null +++ b/src/unittest/libboardgame_util/ArrayListTest.cpp @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_util/ArrayListTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_util/ArrayList.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_util; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(util_array_list_basic) +{ + ArrayList l; + LIBBOARDGAME_CHECK_EQUAL(0u, l.size()); + LIBBOARDGAME_CHECK(l.empty()); + l.push_back(5); + LIBBOARDGAME_CHECK_EQUAL(1u, l.size()); + LIBBOARDGAME_CHECK(! l.empty()); + LIBBOARDGAME_CHECK_EQUAL(5, l[0]); + l.push_back(7); + LIBBOARDGAME_CHECK_EQUAL(2u, l.size()); + LIBBOARDGAME_CHECK(! l.empty()); + LIBBOARDGAME_CHECK_EQUAL(5, l[0]); + LIBBOARDGAME_CHECK_EQUAL(7, l[1]); + l.clear(); + LIBBOARDGAME_CHECK_EQUAL(0u, l.size()); + LIBBOARDGAME_CHECK(l.empty()); +} + +LIBBOARDGAME_TEST_CASE(util_array_list_construct_single_element) +{ + ArrayList l(5); + LIBBOARDGAME_CHECK_EQUAL(1u, l.size()); + LIBBOARDGAME_CHECK_EQUAL(5, l[0]); +} + +LIBBOARDGAME_TEST_CASE(util_array_list_equals) +{ + ArrayList l1{ 1, 2, 3 }; + ArrayList l2{ 1, 2, 3 }; + LIBBOARDGAME_CHECK(l1 == l2); + l2.push_back(4); + LIBBOARDGAME_CHECK(! (l1 == l2)); + l2 = ArrayList({ 2, 1, 3 }); + LIBBOARDGAME_CHECK(! (l1 == l2)); +} + +LIBBOARDGAME_TEST_CASE(util_array_list_pop_back) +{ + ArrayList l(5); + int i = l.pop_back(); + LIBBOARDGAME_CHECK_EQUAL(5, i); + LIBBOARDGAME_CHECK(l.empty()); +} + +LIBBOARDGAME_TEST_CASE(util_array_list_remove) +{ + ArrayList l{ 1, 2, 3, 4 }; + l.remove(2); + LIBBOARDGAME_CHECK_EQUAL(3u, l.size()); + LIBBOARDGAME_CHECK_EQUAL(1, l[0]); + LIBBOARDGAME_CHECK_EQUAL(3, l[1]); + LIBBOARDGAME_CHECK_EQUAL(4, l[2]); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/CMakeLists.txt b/src/unittest/libboardgame_util/CMakeLists.txt new file mode 100644 index 0000000..c157257 --- /dev/null +++ b/src/unittest/libboardgame_util/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(unittest_libboardgame_util + ArrayListTest.cpp + OptionsTest.cpp + StatisticsTest.cpp + StringUtilTest.cpp +) + +target_link_libraries(unittest_libboardgame_util + boardgame_test_main + boardgame_test + boardgame_util + boardgame_sys + ) + +add_test(libboardgame_util unittest_libboardgame_util) diff --git a/src/unittest/libboardgame_util/OptionsTest.cpp b/src/unittest/libboardgame_util/OptionsTest.cpp new file mode 100644 index 0000000..842e9ea --- /dev/null +++ b/src/unittest/libboardgame_util/OptionsTest.cpp @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_util/OptionsTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_util/Options.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_util; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_basic) +{ + vector specs = + { "first|a:", "second|b:", "third|c", "fourth", "fifth" }; + const char* argv[] = + { nullptr, "--second", "secondval", "--first", "firstval", + "--fourth", "-c", "arg1", "arg2" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK(opt.contains("first")); + LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), "firstval"); + LIBBOARDGAME_CHECK(opt.contains("second")); + LIBBOARDGAME_CHECK_EQUAL(opt.get("second"), "secondval"); + LIBBOARDGAME_CHECK(opt.contains("third")); + LIBBOARDGAME_CHECK(opt.contains("fourth")); + LIBBOARDGAME_CHECK(! opt.contains("fifth")); + auto& args = opt.get_args(); + LIBBOARDGAME_CHECK_EQUAL(args.size(), 2u); + LIBBOARDGAME_CHECK_EQUAL(args[0], "arg1"); + LIBBOARDGAME_CHECK_EQUAL(args[1], "arg2"); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_end_options) +{ + vector specs = { "first:" }; + const char* argv[] = + { nullptr, "--first", "firstval", "--", "--arg1" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), "firstval"); + auto& args = opt.get_args(); + LIBBOARDGAME_CHECK_EQUAL(args.size(), 1u); + LIBBOARDGAME_CHECK_EQUAL(args[0], "--arg1"); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_missing_val) +{ + vector specs = { "first:" }; + const char* argv[] = { nullptr, "--first" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + LIBBOARDGAME_CHECK_THROW(Options opt(argc, argv, specs), runtime_error); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_nospace) +{ + vector specs = { "first|a:", "second|b:" }; + const char* argv[] = { nullptr, "-abc" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), "bc"); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_multi_short_with_val) +{ + vector specs = { "first|a", "second|b:" }; + const char* argv[] = { nullptr, "-ab", "c" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK(opt.contains("first")); + LIBBOARDGAME_CHECK_EQUAL(opt.get("second"), "c"); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_type) +{ + vector specs = { "first:", "second:" }; + const char* argv[] = { nullptr, "--first", "10", "--second", "foo" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), 10); + LIBBOARDGAME_CHECK_THROW(opt.get("second"), runtime_error); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/StatisticsTest.cpp b/src/unittest/libboardgame_util/StatisticsTest.cpp new file mode 100644 index 0000000..0084a73 --- /dev/null +++ b/src/unittest/libboardgame_util/StatisticsTest.cpp @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_util/StatisticsTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_util/Statistics.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_util; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(libboardgame_util_statistics_basic) +{ + Statistics s; + s.add(12); + s.add(11); + s.add(14); + s.add(16); + s.add(15); + LIBBOARDGAME_CHECK_EQUAL(s.get_count(), 5.); + LIBBOARDGAME_CHECK_CLOSE_EPS(s.get_mean(), 13.6, 1e-6); + LIBBOARDGAME_CHECK_CLOSE_EPS(s.get_variance(), 3.44, 1e-6); + LIBBOARDGAME_CHECK_CLOSE_EPS(s.get_deviation(), 1.854723, 1e-6); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/StringUtilTest.cpp b/src/unittest/libboardgame_util/StringUtilTest.cpp new file mode 100644 index 0000000..0f10f5f --- /dev/null +++ b/src/unittest/libboardgame_util/StringUtilTest.cpp @@ -0,0 +1,97 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_util/StringUtilTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_util/StringUtil.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_util; + +//---------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(libboardgame_util_get_letter_coord) +{ + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(0), "a"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(1), "b"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(25), "z"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26), "aa"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 + 1), "ab"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 + 25), "az"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(2 * 26), "ba"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(2 * 26 + 1), "bb"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(2 * 26 + 25), "bz"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 * 26), "za"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 * 26 + 1), "zb"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 * 26 + 25), "zz"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(27 * 26), "aaa"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(27 * 26 + 1), "aab"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(27 * 26 + 25), "aaz"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(28 * 26), "aba"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(28 * 26 + 1), "abb"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(28 * 26 + 25), "abz"); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_split) +{ + { + vector v = split("a,b,cc,d", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 4u); + LIBBOARDGAME_CHECK_EQUAL(v[0], "a"); + LIBBOARDGAME_CHECK_EQUAL(v[1], "b"); + LIBBOARDGAME_CHECK_EQUAL(v[2], "cc"); + LIBBOARDGAME_CHECK_EQUAL(v[3], "d"); + } + { + vector v = split("", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 0u); + } + { + vector v = split("a,", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 2u); + LIBBOARDGAME_CHECK_EQUAL(v[0], "a"); + LIBBOARDGAME_CHECK_EQUAL(v[1], ""); + } + { + vector v = split(",a", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 2u); + LIBBOARDGAME_CHECK_EQUAL(v[0], ""); + LIBBOARDGAME_CHECK_EQUAL(v[1], "a"); + } + { + vector v = split("a,,b", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 3u); + LIBBOARDGAME_CHECK_EQUAL(v[0], "a"); + LIBBOARDGAME_CHECK_EQUAL(v[1], ""); + LIBBOARDGAME_CHECK_EQUAL(v[2], "b"); + } +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_to_lower) +{ + LIBBOARDGAME_CHECK_EQUAL(to_lower("AabC "), "aabc "); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_trim) +{ + LIBBOARDGAME_CHECK_EQUAL(trim("aa bb"), "aa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim(" \t\r\naa bb"), "aa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim("aa bb \t\r\n"), "aa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim(""), ""); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_trim_right) +{ + LIBBOARDGAME_CHECK_EQUAL(trim_right("aa bb"), "aa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim_right(" \t\r\naa bb"), " \t\r\naa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim_right("aa bb \t\r\n"), "aa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim_right(""), ""); +} + +//---------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/BoardConstTest.cpp b/src/unittest/libpentobi_base/BoardConstTest.cpp new file mode 100644 index 0000000..e8ed58d --- /dev/null +++ b/src/unittest/libpentobi_base/BoardConstTest.cpp @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/BoardConstTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libpentobi_base/BoardConst.h" + +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libpentobi_base; + +//----------------------------------------------------------------------------- + +/** Test that points in move strings are ordered. + As specified in doc/blksgf/Pentobi-SGF.html, the order should be + (a1, b1, ..., a2, b2, ...). There is no restriction on the order when + parsing move strings in from_string(). */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_const_move_string) +{ + auto& bc = BoardConst::get(Variant::duo); + Move mv = bc.from_string("h7,i7,i6,j6,j5"); + LIBBOARDGAME_CHECK_EQUAL(bc.to_string(mv), "j5,i6,j6,h7,i7"); +} + +/** Check symmetry information in MoveInfoExt for some moves. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_const_symmetry_info) +{ + auto& bc = BoardConst::get(Variant::trigon_2); + auto& info_ext_2 = + bc.get_move_info_ext_2(bc.from_string("q9,q10,r10,q11,r11,s11")); + LIBBOARDGAME_CHECK(! info_ext_2.breaks_symmetry); + LIBBOARDGAME_CHECK_EQUAL(info_ext_2.symmetric_move.to_int(), + bc.from_string("q8,r8,s8,r9,s9,s10").to_int()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/BoardTest.cpp b/src/unittest/libpentobi_base/BoardTest.cpp new file mode 100644 index 0000000..a5b78b0 --- /dev/null +++ b/src/unittest/libpentobi_base/BoardTest.cpp @@ -0,0 +1,165 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/BoardTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libpentobi_base/Board.h" + +#include "libboardgame_test/Test.h" +#include "libpentobi_base/MoveMarker.h" + +using namespace std; +using namespace libpentobi_base; + +//----------------------------------------------------------------------------- + +namespace { + +void play(Board& bd, Color c, const char* s) +{ + bd.play(c, bd.from_string(s)); +} + +} // namespace + +//----------------------------------------------------------------------------- + +/** Check some basic functions in a Classic Two-Player game. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_classic_2) +{ + /* + ( + ;GM[Blokus Two-Player] + ;1[a20,b20,c20,d20,e20] + ;2[q20,r20,s20,t20] + ;3[p1,q1,r1,s1,t1] + ;4[a1,b1,c1,d1] + ;1[f19,g19,h19,i19] + ;2[o19,p19] + ;3[m1,l2,m2,n2,o2] + ;4[e2,f2] + ;1[j18,k18,l18,l19,m19] + ;2[n20] + ;3[h2,i2,i3,j3,k3] + ;4[g1] + ;1[o17,n18,o18,p18,q18] + ;3[d2,d3,e3,f3,g3] + ;1[n13,o13,n14,n15,n16] + ;3[p3,p4,p5,p6] + ;1[n10,n11,o11,p11,p12] + ;3[l4,m4,m5,n5] + ;1[o7,p7,q7,o8,o9] + ;3[j5,k5] + ;1[l6,m6,n6,m7,m8] + ;3[a3,a4,b4,c4] + ;1[i6,j6,j7,k7,j8] + ;3[d5,e5,f5] + ;1[g6,f7,g7,h7] + ;3[j1] + ;1[c6,d6,e6,c7] + ;1[a8,b8,b9,c9] + ;1[d10,e10,d11,e11] + ;1[f9,g9,h9] + ;1[r4,s4,r5,r6,s6] + ;1[t7,s8,t8,r9,s9] + ;1[q13,r13,p14,q14,r14] + ;1[s16,r17,s17,t17,s18] + ;1[l9,k10,l10] + ;1[j11,j12] + ;1[i10] + ) + */ + unique_ptr bd(new Board(Variant::classic_2)); + play(*bd, Color(0), "a20,b20,c20,d20,e20"); + play(*bd, Color(1), "q20,r20,s20,t20"); + play(*bd, Color(2), "p1,q1,r1,s1,t1"); + play(*bd, Color(3), "a1,b1,c1,d1"); + play(*bd, Color(0), "f19,g19,h19,i19"); + play(*bd, Color(1), "o19,p19"); + play(*bd, Color(2), "m1,l2,m2,n2,o2"); + play(*bd, Color(3), "e2,f2"); + play(*bd, Color(0), "j18,k18,l18,l19,m19"); + play(*bd, Color(1), "n20"); + play(*bd, Color(2), "h2,i2,i3,j3,k3"); + play(*bd, Color(3), "g1"); + play(*bd, Color(0), "o17,n18,o18,p18,q18"); + play(*bd, Color(2), "d2,d3,e3,f3,g3"); + play(*bd, Color(0), "n13,o13,n14,n15,n16"); + play(*bd, Color(2), "p3,p4,p5,p6"); + play(*bd, Color(0), "n10,n11,o11,p11,p12"); + play(*bd, Color(2), "l4,m4,m5,n5"); + play(*bd, Color(0), "o7,p7,q7,o8,o9"); + play(*bd, Color(2), "j5,k5"); + play(*bd, Color(0), "l6,m6,n6,m7,m8"); + play(*bd, Color(2), "a3,a4,b4,c4"); + play(*bd, Color(0), "i6,j6,j7,k7,j8"); + play(*bd, Color(2), "d5,e5,f5"); + play(*bd, Color(0), "g6,f7,g7,h7"); + play(*bd, Color(2), "j1"); + play(*bd, Color(0), "c6,d6,e6,c7"); + play(*bd, Color(0), "a8,b8,b9,c9"); + play(*bd, Color(0), "d10,e10,d11,e11"); + play(*bd, Color(0), "f9,g9,h9"); + play(*bd, Color(0), "r4,s4,r5,r6,s6"); + play(*bd, Color(0), "t7,s8,t8,r9,s9"); + play(*bd, Color(0), "q13,r13,p14,q14,r14"); + play(*bd, Color(0), "s16,r17,s17,t17,s18"); + play(*bd, Color(0), "l9,k10,l10"); + play(*bd, Color(0), "j11,j12"); + play(*bd, Color(0), "i10"); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 37u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(109)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(7)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(2)), ScoreType(38)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(3)), ScoreType(7)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(0)), ScoreType(133)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(1)), ScoreType(-133)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(2)), ScoreType(133)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(3)), ScoreType(-133)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(0)), 21u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(1)), 3u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(2)), 10u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(3)), 3u); +} + +LIBBOARDGAME_TEST_CASE(pentobi_base_board_gen_moves_classic_initial) +{ + unique_ptr bd(new Board(Variant::classic)); + unique_ptr moves(new MoveList); + unique_ptr marker(new MoveMarker); + bd->gen_moves(Color(0), *marker, *moves); + LIBBOARDGAME_CHECK_EQUAL(moves->size(), 58u); +} + +/** Test get_place() in a 4-color, 2-player game when the player 1 has + a higher score but color 1 has less points than color 2. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_get_place) +{ + unique_ptr bd(new Board(Variant::classic_2)); + play(*bd, Color(0), "a20,b20"); + play(*bd, Color(1), "r20,s20,t20"); + play(*bd, Color(2), "q1,r1,s1,t1"); + play(*bd, Color(3), "a1,b1"); + // Not a final position but Board::get_place() should not care about that + unsigned place; + bool isPlaceShared; + bd->get_place(Color(0), place, isPlaceShared); + LIBBOARDGAME_CHECK_EQUAL(place, 0u); + LIBBOARDGAME_CHECK(! isPlaceShared); + bd->get_place(Color(1), place, isPlaceShared); + LIBBOARDGAME_CHECK_EQUAL(place, 1u); + LIBBOARDGAME_CHECK(! isPlaceShared); + bd->get_place(Color(2), place, isPlaceShared); + LIBBOARDGAME_CHECK_EQUAL(place, 0u); + LIBBOARDGAME_CHECK(! isPlaceShared); + bd->get_place(Color(3), place, isPlaceShared); + LIBBOARDGAME_CHECK_EQUAL(place, 1u); + LIBBOARDGAME_CHECK(! isPlaceShared); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/BoardUpdaterTest.cpp b/src/unittest/libpentobi_base/BoardUpdaterTest.cpp new file mode 100644 index 0000000..f20d7db --- /dev/null +++ b/src/unittest/libpentobi_base/BoardUpdaterTest.cpp @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/BoardUpdaterTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libpentobi_base/BoardUpdater.h" + +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libpentobi_base; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::get_last_node; + +//----------------------------------------------------------------------------- + +/** Test that BoardUpdater throws an exception if a piece is played twice. + A tree from a file written by another application could contain move + sequences where a piece is played twice. This could break assumptions + about the maximum number of moves in a game at some places in Pentobi's + code, so BoardUpdater should detect this and throw an exception. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_piece_played_twice) +{ + istringstream in("(;GM[Blokus];1[a1];1[a3])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + auto& node = get_last_node(tree.get_root()); + LIBBOARDGAME_CHECK_THROW(updater.update(*bd, tree, node), runtime_error); +} + +/** Test BoardUpdater with setup properties in root node. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup) +{ + istringstream in("(;GM[Blokus Duo]" + "AB[e8,e9,f9,d10,e10][g6,f7,g7,h7,g8]" + "AW[i4,h5,i5,j5,i6][j7,j8,j9,k9,j10])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + updater.update(*bd, tree, tree.get_root()); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 0u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(10)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(10)); +} + +/** Test BoardUpdater with setup properties in an inner node. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup_inner_node) +{ + istringstream in("(;GM[Blokus Duo]" + " ;B[e8,e9,f9,d10,e10]" + " ;AB[g6,f7,g7,h7,g8]AW[i4,h5,i5,j5,i6]" + " ;W[j7,j8,j9,k9,j10])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + auto& node = get_last_node(tree.get_root()); + updater.update(*bd, tree, node); + // BoardUpdater merges setup properties with existing position, so + // get_nu_moves() should return the number of moves played after the setup + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 1u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(10)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(10)); +} + +/** Test removing a piece with the AE property. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup_empty) +{ + istringstream in("(;GM[Blokus Duo]" + " ;B[e8,e9,f9,d10,e10]" + " ;W[j7,j8,j9,k9,j10]" + " ;AE[e8,e9,f9,d10,e10])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + auto& node = get_last_node(tree.get_root()); + updater.update(*bd, tree, node); + // BoardUpdater merges setup properties with existing position, so + // get_nu_moves() should return the number of moves played after the setup + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 0u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(0)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(5)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/CMakeLists.txt b/src/unittest/libpentobi_base/CMakeLists.txt new file mode 100644 index 0000000..1930c00 --- /dev/null +++ b/src/unittest/libpentobi_base/CMakeLists.txt @@ -0,0 +1,19 @@ +add_executable(unittest_libpentobi_base + BoardConstTest.cpp + BoardTest.cpp + BoardUpdaterTest.cpp + GameTest.cpp + TreeTest.cpp +) + +target_link_libraries(unittest_libpentobi_base + boardgame_test_main + pentobi_base + boardgame_base + boardgame_sgf + boardgame_test + boardgame_util + boardgame_sys + ) + +add_test(libpentobi_base unittest_libpentobi_base) diff --git a/src/unittest/libpentobi_base/GameTest.cpp b/src/unittest/libpentobi_base/GameTest.cpp new file mode 100644 index 0000000..e9db78d --- /dev/null +++ b/src/unittest/libpentobi_base/GameTest.cpp @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/GameTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libpentobi_base/Game.h" + +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libpentobi_base; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::get_last_node; + +//----------------------------------------------------------------------------- + +/** Test that the current node is in a defined state if the root node contains + invalid properties. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_game_current_defined_invalid_root) +{ + istringstream in("(;GM[Blokus]1[a99999])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + Game game(Variant::classic); + try + { + game.init(root); + } + catch (const runtime_error&) + { + // ignore + } + LIBBOARDGAME_CHECK_EQUAL(&game.get_current(), &game.get_root()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/TreeTest.cpp b/src/unittest/libpentobi_base/TreeTest.cpp new file mode 100644 index 0000000..1a2270a --- /dev/null +++ b/src/unittest/libpentobi_base/TreeTest.cpp @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/TreeTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_sgf/MissingProperty.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_test/Test.h" +#include "libpentobi_base/PentobiTree.h" + +using namespace std; +using namespace libpentobi_base; +using libboardgame_sgf::InvalidPropertyValue; +using libboardgame_sgf::MissingProperty; +using libboardgame_sgf::TreeReader; + +//----------------------------------------------------------------------------- + +/** Check backwards compatibility to move properties used in Pentobi 0.1. + Pentobi 0.1 used the property id's BLUE,YELLOW,RED,GREEN in four-player + game variants instead of 1,2,3,4. (It also used point lists instead of + single-value move properties. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_tree_backward_compatibility_0_1) +{ + istringstream in("(;GM[Blokus Two-Player];BLUE[a16][a17][a18][a19][a20]" + ";YELLOW[s17][t17][t18][t19][t20];RED[t1][t2][t3][t4][t5]" + ";GREEN[a1][b1][c1][d1][d2])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + auto& bc = tree.get_board_const(); + auto& geo = bc.get_geometry(); + auto node = &tree.get_root(); + node = &node->get_child(); + { + auto mv = tree.get_move(*node); + LIBBOARDGAME_CHECK(! mv.is_null()); + LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(0)); + auto points = bc.get_move_points(mv.move); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 4))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 3))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 2))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 1))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 0))); + } + node = &node->get_child(); + { + auto mv = tree.get_move(*node); + LIBBOARDGAME_CHECK(! mv.is_null()); + LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(1)); + auto points = bc.get_move_points(mv.move); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(18, 3))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 3))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 2))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 1))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 0))); + } + node = &node->get_child(); + { + auto mv = tree.get_move(*node); + LIBBOARDGAME_CHECK(! mv.is_null()); + LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(2)); + auto points = bc.get_move_points(mv.move); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 19))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 18))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 17))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 16))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 15))); + } + node = &node->get_child(); + { + auto mv = tree.get_move(*node); + LIBBOARDGAME_CHECK(! mv.is_null()); + LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(3)); + auto points = bc.get_move_points(mv.move); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 19))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(1, 19))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(2, 19))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(3, 19))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(3, 18))); + } +} + +/** Check that Tree constructor throws InvalidPropertyValue on unknown GM + property value. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_tree_invalid_game) +{ + istringstream in("(;GM[1])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + LIBBOARDGAME_CHECK_THROW(PentobiTree tree(root), InvalidPropertyValue); +} + +/** Check that Tree constructor throws MissingProperty on missing GM + property. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_tree_missing_game_property) +{ + istringstream in("(;)"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + LIBBOARDGAME_CHECK_THROW(PentobiTree tree(root), MissingProperty); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_mcts/CMakeLists.txt b/src/unittest/libpentobi_mcts/CMakeLists.txt new file mode 100644 index 0000000..3879222 --- /dev/null +++ b/src/unittest/libpentobi_mcts/CMakeLists.txt @@ -0,0 +1,20 @@ +add_executable(unittest_libpentobi_mcts + SearchTest.cpp +) + +target_link_libraries(unittest_libpentobi_mcts + boardgame_test_main + pentobi_mcts + pentobi_base + boardgame_base + boardgame_sgf + boardgame_test + boardgame_util + boardgame_sys + ) + +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(unittest_libpentobi_mcts ${CMAKE_THREAD_LIBS_INIT}) +endif() + +add_test(libpentobi_mcts unittest_libpentobi_mcts) diff --git a/src/unittest/libpentobi_mcts/SearchTest.cpp b/src/unittest/libpentobi_mcts/SearchTest.cpp new file mode 100644 index 0000000..ab4333a --- /dev/null +++ b/src/unittest/libpentobi_mcts/SearchTest.cpp @@ -0,0 +1,115 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_mcts/SearchTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libpentobi_mcts/Search.h" + +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_test/Test.h" +#include "libboardgame_util/CpuTimeSource.h" +#include "libpentobi_base/BoardUpdater.h" +#include "libpentobi_base/PentobiTree.h" + +using namespace std; +using namespace libpentobi_mcts; +using libboardgame_sgf::SgfNode; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::get_last_node; +using libboardgame_util::CpuTimeSource; +using libpentobi_base::BoardUpdater; +using libpentobi_base::PentobiTree; + +//----------------------------------------------------------------------------- + +/** Test that state generates a playout move even if no large pieces are + playable early in the game. + This tests for a bug that occurred in Pentobi 1.1 with game variant Trigon: + Because moves that are below a certain piece size are not generated early + in the game, it could happen in rare cases that no moves were generated + at all. */ +LIBBOARDGAME_TEST_CASE(pentobi_mcts_search_no_large_pieces) +{ + istringstream + in(R"delim( + (;GM[Blokus Trigon Two-Player];1[r4,r5,s5,r6,s6,r7] + ;2[r12,q13,r13,q14,r14,r15];3[k11,l11,m11,n11,j12,k12] + ;4[w7,x7,y7,z7,v8,w8];1[s8,t8,r9,s9,t9,u9] + ;2[n12,o12,m13,n13,o13,o14];3[k13,k14,l14,l15,m15,n15] + ;4[w9,t10,u10,v10,w10,x10];1[n10,o10,p10,q10,r10,r11] + ;2[o15,k16,l16,m16,n16,o16];3[i15,j15,h16,i16,j16,j17] + ;4[u11,s12,t12,u12,v12,v13];1[p4,m5,n5,o5,p5,m6] + ;2[k17,i18,j18,k18,l18,m18];3[l17,m17,n17,o17,p17,o18] + ;4[t14,u14,s15,t15,r16,s16];1[l8,m8,j9,k9,l9,m9]) + )delim"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + updater.update(*bd, tree, get_last_node(tree.get_root())); + unsigned nu_threads = 1; + size_t memory = 10000; + unique_ptr search(new Search(bd->get_variant(), nu_threads, + memory)); + Float max_count = 1; + size_t min_simulations = 1; + double max_time = 0; + CpuTimeSource time_source; + Move mv; + bool res = search->search(mv, *bd, Color(1), max_count, min_simulations, + max_time, time_source); + LIBBOARDGAME_CHECK(res); + LIBBOARDGAME_CHECK(! mv.is_null()); +} + +/** Test that useless one-piece moves are generated if no other moves exist. + Useless one-piece moves (all neighbors occupied) are not needed during + the search, but the search should still return one if no other legal + moves exist. */ +LIBBOARDGAME_TEST_CASE(pentobi_mcts_search_callisto_useless_one_piece) +{ + istringstream + in(R"delim( + (;GM[Callisto Two-Player];1[k10];2[k7];1[g6];2[g11] + ;1[f7,g7,h7,f8,h8];2[d9,e9,e10,f10,f11];1[c8,d8,e8,c9] + ;2[k8,l8,m8,l9,l10];1[j11,k11,i12,j12];2[h11,i11,h12,h13,i13] + ;1[n9,m10,n10,l11,m11];2[j4,j5,j6,k6];1[j13,h14,i14,j14,j15] + ;2[h3,g4,h4,i4,h5];1[n6,m7,n7,o7,n8];2[f13,g13,f14,g14] + ;1[c10,d10,c11,d11];2[e5,f5,g5,f6];1[l5,m5,l6,m6];2[e6,c7,d7,e7] + ;1[j3,k3,k4,k5];2[h1,i1,h2,i2];1[e11,e12,f12,e13];2[i8,h9,i9,h10] + ;1[b7,a8,b8,a9];2[k12];1[g15,h15,i15,h16];2[l12,m12,k13,l13] + ;1[j8,j9,j10];2[i5,h6,i6,i7];1[g8,g9,g10];2[g2,f3,g3];1[o9,p9,o10] + ;2[d5,c6,d6];1[b9,b10];2[e4,f4];1[o8,p8]) + )delim"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + updater.update(*bd, tree, get_last_node(tree.get_root())); + unsigned nu_threads = 1; + size_t memory = 10000; + unique_ptr search(new Search(bd->get_variant(), nu_threads, + memory)); + Float max_count = 1; + size_t min_simulations = 1; + double max_time = 0; + CpuTimeSource time_source; + Move mv; + bool res = search->search(mv, *bd, Color(0), max_count, min_simulations, + max_time, time_source); + LIBBOARDGAME_CHECK(res); + LIBBOARDGAME_CHECK(! mv.is_null()); + LIBBOARDGAME_CHECK(bd->get_move_piece(mv) == bd->get_one_piece()); +} + +//----------------------------------------------------------------------------- diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..ef13284 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,20 @@ +# Build the NSIS installer +# We assume dynamic linking and add a custom target that runs makensis and +# uses windeployqt to include the Qt libraries. + +get_target_property(QMAKE Qt5::qmake LOCATION) +find_program(WINDEPLOYQT windeployqt.exe HINTS "${QMAKE}") + +set(X86 "(x86)") +find_program(MAKENSIS makensis + PATHS "$ENV{ProgramFiles}\\NSIS" "$ENV{ProgramFiles${X86}}\\NSIS") + +add_custom_target(nsis + COMMAND ${CMAKE_COMMAND} -E remove_directory deploy + COMMAND ${CMAKE_COMMAND} -E make_directory deploy + COMMAND ${CMAKE_COMMAND} -E copy "$" deploy + COMMAND ${WINDEPLOYQT} --dir deploy --release --no-svg "deploy/pentobi.exe" + COMMAND ${MAKENSIS} install.nsis +) + +configure_file(install.nsis.in install.nsis @ONLY) diff --git a/windows/German.nsh b/windows/German.nsh new file mode 100644 index 0000000..6963e5f --- /dev/null +++ b/windows/German.nsh @@ -0,0 +1,10 @@ +; German translations +; NSIS version 2.46 does not support Unicode yet, so this file needs to be +; encoded in ISO 8859 + +LangString ADD_START_MENU_ENTRY ${LANG_GERMAN} \ + "Eintrag im Startmenü hinzufügen" +LangString CREATE_DESKTOP_SHORTCUT ${LANG_GERMAN} \ + "Desktopverknüpfung erstellen" +LangString INSTALLER_TITLE ${LANG_GERMAN} \ + "Pentobi ${PENTOBI_VERSION} installieren" diff --git a/windows/blksgf.ico b/windows/blksgf.ico new file mode 100644 index 0000000..86b99c9 Binary files /dev/null and b/windows/blksgf.ico differ diff --git a/windows/install.nsis.in b/windows/install.nsis.in new file mode 100644 index 0000000..12ef4e9 --- /dev/null +++ b/windows/install.nsis.in @@ -0,0 +1,143 @@ +; Script for creating a Windows installer with NSIS (http://nsis.sf.net) + +!define PENTOBI_VERSION "@PENTOBI_VERSION@" +!define PENTOBI_SRC_DIR "@CMAKE_SOURCE_DIR@" +!define PENTOBI_BUILD_DIR "@CMAKE_BINARY_DIR@" + +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\Pentobi" + +SetCompressor /SOLID lzma + +!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\orange-install.ico" +!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\orange-uninstall.ico" +!define MUI_WELCOMEFINISHPAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Wizard\orange.bmp" +!define MUI_COMPONENTSPAGE_NODESC +!include "MUI.nsh" +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE "${PENTOBI_SRC_DIR}\COPYING" +!insertmacro MUI_PAGE_COMPONENTS +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!define MUI_FINISHPAGE_RUN "$INSTDIR\Pentobi.exe" +!insertmacro MUI_PAGE_FINISH +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "German" +!insertmacro MUI_RESERVEFILE_INSTALLOPTIONS + +!define ADD_START_MENU_ENTRY_DEFAULT "Add start menu entry" +!define CREATE_DESKTOP_SHORTCUT_DEFAULT "Create desktop shortcut" +!define INSTALLER_TITLE_DEFAULT "Pentobi ${PENTOBI_VERSION} Installer" +LangString ADD_START_MENU_ENTRY ${LANG_ENGLISH} \ + "${ADD_START_MENU_ENTRY_DEFAULT}" +LangString CREATE_DESKTOP_SHORTCUT ${LANG_ENGLISH} \ + "${CREATE_DESKTOP_SHORTCUT_DEFAULT}" +LangString INSTALLER_TITLE ${LANG_ENGLISH} \ + "${INSTALLER_TITLE_DEFAULT}" +!include "${PENTOBI_SRC_DIR}\windows\German.nsh" + +Name "Pentobi" +Caption "$(INSTALLER_TITLE)" +OutFile "pentobi-${PENTOBI_VERSION}-install.exe" +InstallDir "$PROGRAMFILES\Pentobi" +InstallDirRegKey HKLM "Software\Pentobi" "" +; Set admin level, needed for shortcut removal on Vista +; (http://nsis.sf.net/Shortcuts_removal_fails_on_Windows_Vista) +RequestExecutionLevel admin + +Section + +IfFileExists "$INSTDIR\Uninstall.exe" 0 +2 +ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR' + +SetOutPath "$INSTDIR\translations" +File "${PENTOBI_BUILD_DIR}\src\libpentobi_gui\*.qm" +File "${PENTOBI_BUILD_DIR}\src\pentobi\*.qm" +SetOutPath "$INSTDIR\books" +File "${PENTOBI_SRC_DIR}\src\books\book_*.blksgf" +SetOutPath "$INSTDIR" +File /r "${PENTOBI_SRC_DIR}\src\pentobi\help" +File /oname=COPYING.txt "${PENTOBI_SRC_DIR}\COPYING" +File /oname=Pentobi.exe "${PENTOBI_BUILD_DIR}\windows\deploy\pentobi.exe" +File "${PENTOBI_SRC_DIR}\src\pentobi\pentobi.ico" +SetOutPath "$INSTDIR" +File "${PENTOBI_BUILD_DIR}\windows\deploy\*.dll" +SetOutPath "$INSTDIR\imageformats" +File "${PENTOBI_BUILD_DIR}\windows\deploy\imageformats\*.dll" +SetOutPath "$INSTDIR\platforms" +File "${PENTOBI_BUILD_DIR}\windows\deploy\platforms\*.dll" +SetOutPath "$INSTDIR\translations" +File "${PENTOBI_BUILD_DIR}\windows\deploy\translations\qt_de.qm" + +WriteRegStr HKLM "Software\Pentobi" "" $INSTDIR + +WriteUninstaller $INSTDIR\Uninstall.exe +WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "Pentobi" +WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${PENTOBI_VERSION}" +WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\pentobi.ico" +WriteRegStr HKLM "${UNINST_KEY}" "URLInfoAbout" "http://pentobi.sf.net/" +WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe" + +SetOutPath "$INSTDIR" +File "${PENTOBI_SRC_DIR}\windows\blksgf.ico" + +WriteRegStr HKCR ".blksgf" "" "Pentobi" +WriteRegStr HKCR ".blksgf" "Content Type" "application/x-blokus-sgf" +WriteRegStr HKCR "Pentobi" "" "Blokus Game" +WriteRegStr HKCR "Pentobi\DefaultIcon" "" "$INSTDIR\blksgf.ico" +WriteRegStr HKCR "Pentobi\shell\open\command" "" \ + "$\"$INSTDIR\Pentobi.exe$\" $\"%1$\"" + +WriteRegStr HKCR "MIME\Database\Content Type\application/x-blokus-sgf" \ + "Extension" ".blksgf" + +WriteRegStr HKCR "Applications\Pentobi.exe" "SupportedTypes" ".blksgf" +WriteRegStr HKCR "Applications\Pentobi\shell\open\command" "" \ + "$\"$INSTDIR\Pentobi.exe$\" $\"%1$\"" + +SectionEnd + +Section "$(ADD_START_MENU_ENTRY)" + +SetShellVarContext all +CreateDirectory "$SMPROGRAMS\Games" +CreateShortCut "$SMPROGRAMS\Games\Pentobi.lnk" "$INSTDIR\Pentobi.exe" + +SectionEnd + +Section "$(CREATE_DESKTOP_SHORTCUT)" + +SetShellVarContext all +CreateShortCut "$DESKTOP\Pentobi.lnk" "$INSTDIR\Pentobi.exe" + +SectionEnd + +Section "Uninstall" + +Delete "$INSTDIR\Uninstall.exe" +Delete "$INSTDIR\Pentobi.exe" +Delete "$INSTDIR\COPYING.txt" +Delete "$INSTDIR\pentobi.ico" +Delete "$INSTDIR\blksgf.ico" +Delete "$INSTDIR\*.dll" +RmDir /r "$INSTDIR\books" +RmDir /r "$INSTDIR\translations" +RmDir /r "$INSTDIR\help" +RmDir /r "$INSTDIR\platforms" +RmDir /r "$INSTDIR\imageformats" +RmDir /r "$INSTDIR\plugins" +RmDir "$INSTDIR" + +SetShellVarContext all +Delete "$SMPROGRAMS\Games\Pentobi.lnk" +Delete "$DESKTOP\Pentobi.lnk" + +DeleteRegKey HKLM "Software\Pentobi" +DeleteRegKey HKLM "${UNINST_KEY}" +DeleteRegKey HKCR "Pentobi" +DeleteRegKey HKCR "Applications\Pentobi.exe" +DeleteRegKey HKCR "Applications\Pentobi" + +SectionEnd